├── Website └── builds.json ├── XProxy.Core ├── GlobalUsigns.cs ├── Interfaces │ ├── IConfig.cs │ └── IClientHandler.cs ├── Core │ ├── Events │ │ ├── BaseEvent.cs │ │ ├── BaseCancellableEvent.cs │ │ ├── Args │ │ │ ├── PlayerAssignTargetServer.cs │ │ │ ├── ProxyStartedListening.cs │ │ │ ├── PlayerCanJoinEvent.cs │ │ │ └── ProxyConnectionRequest.cs │ │ └── EventManager.cs │ ├── Connections │ │ ├── Responses │ │ │ ├── BaseResponse.cs │ │ │ ├── ServerIsFullResponse.cs │ │ │ ├── ServerIsOfflineResponse.cs │ │ │ └── BannedResponse.cs │ │ ├── SimulatedConnection.cs │ │ ├── ProxiedConnection.cs │ │ ├── BaseConnection.cs │ │ ├── ConnectionValidator.cs │ │ ├── LostConnection.cs │ │ ├── QueueConnection.cs │ │ └── LobbyConnection.cs │ ├── Plugin.cs │ └── PluginT.cs ├── Enums │ ├── ConnectionType.cs │ ├── ClientType.cs │ ├── ChallengeType.cs │ ├── ServerStatus.cs │ ├── LostConnectionType.cs │ ├── CentralAuthPreauthFlags.cs │ └── RejectionReason.cs ├── Models │ ├── LastServerInfo.cs │ ├── AuthRejectModel.cs │ ├── PublicKeyResponseModel.cs │ ├── UserModel.cs │ ├── AuthPlayersModel.cs │ ├── PlayerListSerializedModel.cs │ ├── AuthPlayerModel.cs │ ├── AuthResponseModel.cs │ ├── PluginExtensionModel.cs │ ├── CustomNetLogger.cs │ ├── ScpServerList.cs │ ├── QueueTicket.cs │ ├── ServerModel.cs │ ├── ConfigModel.cs │ ├── ListenerServer.cs │ ├── PreAuthModel.cs │ └── MessagesModel.cs ├── Attributes │ └── ConsoleCommand.cs ├── Monitors │ ├── TaskMonitor.cs │ └── CpuProfiler.cs ├── Services │ ├── ClientsUpdaterService.cs │ ├── CleanupService.cs │ ├── ListenersService.cs │ ├── LoggingService.cs │ ├── QueueService.cs │ ├── PublicKeyService.cs │ ├── ConfigService.cs │ ├── CommandsService.cs │ └── PluginsService.cs ├── Properties │ └── PublishProfiles │ │ ├── Linux.pubxml │ │ └── Windows.pubxml ├── Misc │ ├── BuildInformation.cs │ ├── PlaceHolders.cs │ ├── CustomUnbatcher.cs │ ├── CustomBatcher.cs │ ├── HintBuilder.cs │ ├── Logger.cs │ ├── Extensions.cs │ └── CentralServerKeyCache.cs ├── Serialization │ ├── CommentsObjectGraphVisitor.cs │ ├── YamlParser.cs │ ├── CommentsObjectDescriptor.cs │ ├── CommentGatheringTypeInspector.cs │ └── CommentsPropertyDescriptor.cs ├── XProxy.Core.csproj ├── Initializer.cs └── Cryptography │ ├── Sha.cs │ └── ECDSA.cs ├── Storage ├── gameVersions.json └── egg-s-c-p--s-l-proxy.json ├── XProxy ├── Models │ ├── BuildFile.cs │ ├── BuildsListing.cs │ ├── OsSpecificFiles.cs │ ├── BuildInfo.cs │ └── LauncherSettings.cs ├── XProxy.csproj ├── Properties │ └── PublishProfiles │ │ ├── Linux-Build.pubxml │ │ └── Windows-Build.pubxml ├── Misc │ ├── CustomProgressReporter.cs │ ├── Extensions.cs │ └── ConsoleLogger.cs ├── Serialization │ └── YamlParser.cs ├── Program.cs └── Services │ └── UpdaterService.cs ├── XProxy.BuildRelease ├── action.yml ├── Dockerfile ├── Models │ └── BuildInfo.cs ├── XProxy.BuildRelease.csproj └── Program.cs ├── XProxy.BuildListing ├── Models │ ├── BuildsListing.cs │ └── BuildInfo.cs ├── Dockerfile ├── action.yml ├── XProxy.BuildListing.csproj └── Program.cs ├── XProxy.Plugin ├── Core │ ├── ServerStatus.cs │ └── ProxyConnection.cs ├── Config.cs ├── XProxy.Plugin.csproj └── MainClass.cs ├── XProxy.Shared └── XProxy.Shared.csproj ├── .github └── workflows │ ├── listing.yml │ └── release.yml ├── .gitattributes ├── README.md ├── .gitignore └── XProxy.sln /Website/builds.json: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /XProxy.Core/GlobalUsigns.cs: -------------------------------------------------------------------------------- 1 | global using XProxy.Shared; -------------------------------------------------------------------------------- /Storage/gameVersions.json: -------------------------------------------------------------------------------- 1 | [ 2 | "14.0.2", 3 | "13.5.1" 4 | ] 5 | -------------------------------------------------------------------------------- /XProxy.Core/Interfaces/IConfig.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Interfaces 2 | { 3 | public interface IConfig 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XProxy/Models/BuildFile.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Models 2 | { 3 | public class BuildFile 4 | { 5 | public string Url { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Events/BaseEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XProxy.Core.Events 4 | { 5 | public class BaseEvent : EventArgs 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /XProxy.Core/Enums/ConnectionType.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Enums 2 | { 3 | public enum ConnectionType 4 | { 5 | Proxied, 6 | Simulated, 7 | } 8 | } -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/Responses/BaseResponse.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Core.Connections.Responses 2 | { 3 | public class BaseResponse 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XProxy.Core/Enums/ClientType.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Enums 2 | { 3 | public enum ClientType : byte 4 | { 5 | GameClient, 6 | VerificationService, 7 | Proxy, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /XProxy.BuildRelease/action.yml: -------------------------------------------------------------------------------- 1 | name: "Build release" 2 | description: "Generates release build." 3 | branding: 4 | icon: file 5 | color: blue 6 | runs: 7 | using: "docker" 8 | image: "Dockerfile" -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/Responses/ServerIsFullResponse.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Core.Connections.Responses 2 | { 3 | public class ServerIsFullResponse : BaseResponse 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/Responses/ServerIsOfflineResponse.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Core.Connections.Responses 2 | { 3 | public class ServerIsOfflineResponse : BaseResponse 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /XProxy/Models/BuildsListing.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Models 2 | { 3 | public class BuildsListing 4 | { 5 | public Dictionary Builds { get; set; } = new Dictionary(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /XProxy.BuildListing/Models/BuildsListing.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Models 2 | { 3 | public class BuildsListing 4 | { 5 | public Dictionary Builds { get; set; } = new Dictionary(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /XProxy.Core/Enums/ChallengeType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace XProxy.Enums 6 | { 7 | public enum ChallengeType : byte 8 | { 9 | Reply, 10 | MD5, 11 | SHA1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XProxy.Core/Models/LastServerInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XProxy.Models 4 | { 5 | public class LastServerInfo 6 | { 7 | public string Index { get; set; } 8 | public DateTime Time { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /XProxy/Models/OsSpecificFiles.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Models 2 | { 3 | public class OsSpecificFiles 4 | { 5 | public BuildFile Proxy { get; set; } = new BuildFile(); 6 | public BuildFile Dependencies { get; set; } = new BuildFile(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /XProxy.Plugin/Core/ServerStatus.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Enums 2 | { 3 | public enum ServerStatus 4 | { 5 | Starting, 6 | WaitingForPlayers, 7 | StartingRound, 8 | RoundInProgress, 9 | RoundEnding, 10 | RoundRestart, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /XProxy.Core/Enums/ServerStatus.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Enums 2 | { 3 | public enum ServerStatus : byte 4 | { 5 | Starting, 6 | WaitingForPlayers, 7 | StartingRound, 8 | RoundInProgress, 9 | RoundEnding, 10 | RoundRestart, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /XProxy.Core/Interfaces/IClientHandler.cs: -------------------------------------------------------------------------------- 1 | using LiteNetLib; 2 | using XProxy.Models; 3 | 4 | namespace XProxy.Interfaces 5 | { 6 | public interface IClientHandler 7 | { 8 | void CreateConnection(); 9 | void OnReceiveDataFromProxy(NetPacketReader reader, DeliveryMethod method); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /XProxy.Core/Enums/LostConnectionType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace XProxy.Enums 8 | { 9 | public enum LostConnectionType 10 | { 11 | Timeout, 12 | Shutdown 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XProxy.BuildListing/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env 2 | WORKDIR /app 3 | 4 | COPY . ./ 5 | 6 | RUN dotnet publish -c Release -o out 7 | 8 | FROM mcr.microsoft.com/dotnet/runtime:9.0 9 | WORKDIR /app 10 | 11 | COPY --from=build-env /app/out . 12 | 13 | ENTRYPOINT ["dotnet", "/app/XProxy.BuildListing.dll"] -------------------------------------------------------------------------------- /XProxy.BuildRelease/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env 2 | WORKDIR /app 3 | 4 | COPY . ./ 5 | 6 | RUN dotnet publish -c Release -o out 7 | 8 | FROM mcr.microsoft.com/dotnet/runtime:9.0 9 | WORKDIR /app 10 | 11 | COPY --from=build-env /app/out . 12 | 13 | ENTRYPOINT ["dotnet", "/app/XProxy.BuildRelease.dll"] -------------------------------------------------------------------------------- /XProxy.BuildListing/action.yml: -------------------------------------------------------------------------------- 1 | name: "Build listing" 2 | description: "Generates listing for all builds." 3 | branding: 4 | icon: file 5 | color: blue 6 | inputs: 7 | token: 8 | description: "GH Token." 9 | required: true 10 | runs: 11 | using: "docker" 12 | image: "Dockerfile" 13 | args: 14 | - "--token" 15 | - ${{ inputs.token }} -------------------------------------------------------------------------------- /XProxy.Core/Core/Events/BaseCancellableEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace XProxy.Core.Events 8 | { 9 | public class BaseCancellableEvent : BaseEvent 10 | { 11 | public bool IsCancelled { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /XProxy.Core/Models/AuthRejectModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace XProxy.Models 8 | { 9 | public class AuthRejectModel 10 | { 11 | public string Id { get; set; } 12 | public string Reason { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XProxy.Core/Enums/CentralAuthPreauthFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XProxy.Enums 4 | { 5 | [Flags] 6 | public enum CentralAuthPreauthFlags : byte 7 | { 8 | None = 0, 9 | ReservedSlot = 1, 10 | IgnoreBans = 2, 11 | IgnoreWhitelist = 4, 12 | IgnoreGeoblock = 8, 13 | GloballyBanned = 16, 14 | NorthwoodStaff = 32, 15 | AuthRejected = 64 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /XProxy.Core/Attributes/ConsoleCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XProxy.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Method)] 6 | public class ConsoleCommand : Attribute 7 | { 8 | public string Name { get; set; } 9 | 10 | public ConsoleCommand(string name) 11 | { 12 | Name = name; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XProxy.Shared/XProxy.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/SimulatedConnection.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Connections 2 | { 3 | public class SimulatedConnection : BaseConnection 4 | { 5 | public SimulatedConnection(Player plr) : base(plr) 6 | { 7 | plr.InternalDestroyNetwork(); 8 | plr.InternalAcceptConnection(this); 9 | plr.ProcessMirrorMessagesFromProxy = true; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /XProxy.Core/Models/PublicKeyResponseModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace XProxy.Models 8 | { 9 | public class PublicKeyResponseModel 10 | { 11 | public string Key { get; set; } 12 | public string Signature { get; set; } 13 | public string Credits { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XProxy.Core/Models/UserModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace XProxy.Core.Models 9 | { 10 | public class UserModel 11 | { 12 | [Description("If player can join when maintenance is enabled.")] 13 | public bool IgnoreMaintenance { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Events/Args/PlayerAssignTargetServer.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Core.Events.Args 2 | { 3 | public class PlayerAssignTargetServer : BaseEvent 4 | { 5 | public PlayerAssignTargetServer(Player player, Server server) 6 | { 7 | Player = player; 8 | Server = server; 9 | } 10 | 11 | public Player Player { get; } 12 | public Server Server { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /XProxy.Core/Models/AuthPlayersModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json.Serialization; 6 | using System.Threading.Tasks; 7 | 8 | namespace XProxy.Models 9 | { 10 | public class AuthPlayersModel 11 | { 12 | [JsonPropertyName("objects")] 13 | public AuthPlayerModel[] Players { get; set; } = new AuthPlayerModel[0]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XProxy.Core/Models/PlayerListSerializedModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json.Serialization; 6 | using System.Threading.Tasks; 7 | 8 | namespace XProxy.Models 9 | { 10 | public class PlayerListSerializedModel 11 | { 12 | [JsonPropertyName("objects")] 13 | public List UserIds { get; set; } = new List(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Events/Args/ProxyStartedListening.cs: -------------------------------------------------------------------------------- 1 | using XProxy.Core.Events; 2 | 3 | namespace XProxy.Core.Core.Events.Args 4 | { 5 | public class ProxyStartedListening : BaseEvent 6 | { 7 | public ProxyStartedListening(Listener server, int port) 8 | { 9 | Server = server; 10 | Port = port; 11 | } 12 | 13 | public Listener Server { get; private set; } 14 | public int Port { get; private set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/Responses/BannedResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XProxy.Core.Core.Connections.Responses 4 | { 5 | public class BannedResponse : BaseResponse 6 | { 7 | public BannedResponse(string reason, DateTime banExpireDate) 8 | { 9 | Reason = reason; 10 | BanExpiration = banExpireDate; 11 | } 12 | 13 | public string Reason { get; } 14 | 15 | public DateTime BanExpiration { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /XProxy.Core/Models/AuthPlayerModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace XProxy.Models 8 | { 9 | public class AuthPlayerModel 10 | { 11 | public string Id { get; set; } 12 | public string Ip { get; set; } 13 | public string RequestIp { get; set; } 14 | public string Asn { get; set; } 15 | public string AuthSerial { get; set; } 16 | public string VacSession { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /XProxy.Core/Enums/RejectionReason.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Enums 2 | { 3 | public enum RejectionReason : byte 4 | { 5 | NotSpecified, 6 | ServerFull, 7 | InvalidToken, 8 | VersionMismatch, 9 | Error, 10 | AuthenticationRequired, 11 | Banned, 12 | NotWhitelisted, 13 | GloballyBanned, 14 | Geoblocked, 15 | Custom, 16 | ExpiredAuth, 17 | RateLimit, 18 | Challenge, 19 | InvalidChallengeKey, 20 | InvalidChallenge, 21 | Redirect, 22 | Delay, 23 | VerificationAccepted, 24 | VerificationRejected, 25 | CentralServerAuthRejected 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Events/Args/PlayerCanJoinEvent.cs: -------------------------------------------------------------------------------- 1 | using XProxy.Core.Events; 2 | using XProxy.Models; 3 | 4 | namespace XProxy.Core.Core.Events.Args 5 | { 6 | public class PlayerCanJoinEvent : BaseEvent 7 | { 8 | public PlayerCanJoinEvent(Player player, Server server) 9 | { 10 | Player = player; 11 | Server = server; 12 | } 13 | 14 | public Player Player { get; } 15 | public Server Server { get; } 16 | 17 | public bool ForceAllow { get; set; } 18 | public bool ForceDeny { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /XProxy/XProxy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /XProxy.Core/Monitors/TaskMonitor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Threading.Tasks; 3 | 4 | namespace XProxy.Core.Monitors 5 | { 6 | 7 | public class TaskMonitor 8 | { 9 | private static ConcurrentDictionary _activeTasks = new ConcurrentDictionary(); 10 | 11 | public static void RegisterTask(Task task) 12 | { 13 | _activeTasks.TryAdd(task.Id, task); 14 | task.ContinueWith(t => _activeTasks.TryRemove(t.Id, out _)); 15 | } 16 | 17 | public static int GetRunningTaskCount() 18 | { 19 | return _activeTasks.Count; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /XProxy/Properties/PublishProfiles/Linux-Build.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net7.0\publish\linux-x64\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net7.0 13 | linux-x64 14 | false 15 | true 16 | 17 | -------------------------------------------------------------------------------- /XProxy.Core/Services/ClientsUpdaterService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using XProxy.Core; 5 | 6 | namespace XProxy.Services 7 | { 8 | public class ClientsUpdaterService : BackgroundService 9 | { 10 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 11 | { 12 | while (!stoppingToken.IsCancellationRequested) 13 | { 14 | foreach(Player plr in Player.List) 15 | { 16 | plr?.InternalUpdate(); 17 | } 18 | 19 | await Task.Delay(1000); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /XProxy.Core/Properties/PublishProfiles/Linux.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net7\publish\linux-x64\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net7 13 | linux-x64 14 | true 15 | false 16 | false 17 | 18 | -------------------------------------------------------------------------------- /XProxy/Misc/CustomProgressReporter.cs: -------------------------------------------------------------------------------- 1 | using XProxy.Misc; 2 | 3 | public class CustomProgressReporter : IProgress 4 | { 5 | private int current = 0; 6 | 7 | public CustomProgressReporter(string text, string tag) 8 | { 9 | Text = text; 10 | } 11 | 12 | public string Text { get; } 13 | 14 | public void Report(float value) 15 | { 16 | int percentage = (int)(100 * value) / 1; 17 | 18 | int reportEveryNum = percentage/20; 19 | 20 | if (current != reportEveryNum) 21 | { 22 | current = reportEveryNum; 23 | ConsoleLogger.Info(Text.Replace("%percentage%", percentage.ToString()), "XProxy"); 24 | } 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /XProxy/Properties/PublishProfiles/Windows-Build.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net7.0\publish\win-x64\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net7.0 13 | win-x64 14 | false 15 | true 16 | false 17 | 18 | -------------------------------------------------------------------------------- /XProxy.Core/Properties/PublishProfiles/Windows.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net7\publish\win-x64\ 10 | FileSystem 11 | <_TargetId>Folder 12 | net7 13 | win-x64 14 | true 15 | false 16 | false 17 | false 18 | 19 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | 4 | namespace XProxy.Core 5 | { 6 | public abstract class Plugin 7 | { 8 | public virtual string Name { get; } 9 | public virtual string Description { get; } 10 | public virtual string Author { get; } 11 | public virtual Version Version { get; } 12 | 13 | public string PluginDirectory { get; internal set; } 14 | 15 | public virtual void LoadConfig() { } 16 | public virtual void SaveConfig() { } 17 | 18 | public virtual void OnLoad(IServiceCollection serviceCollection) 19 | { 20 | 21 | } 22 | 23 | public virtual void OnUnload() 24 | { 25 | 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /XProxy.Core/Models/AuthResponseModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace XProxy.Models 8 | { 9 | public class AuthResponseModel 10 | { 11 | public bool Success { get; set; } 12 | public bool Verified { get; set; } 13 | public string Error { get; set; } 14 | public string Token { get; set; } 15 | public string[] Messages { get; set; } 16 | public string[] Actions { get; set; } 17 | public string[] AuthAccepted { get; set; } 18 | public AuthRejectModel[] AuthRejected { get; set; } 19 | public string VerificationChallenge { get; set; } 20 | public string VerificationResponse { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /XProxy.Core/Models/PluginExtensionModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace XProxy.Core.Models 4 | { 5 | public class PluginExtensionModel 6 | { 7 | [Description("ConnectionKey is used for secure connection, plugin needs to send same key to make connection with proxy.")] 8 | public string ConnectionKey { get; set; } = ""; 9 | 10 | [Description("AllowedConnections can limit which ips are allowed to connect to proxy.")] 11 | public string[] AllowedConnections { get; set; } = new string[0]; 12 | 13 | [Description("Normally by default proxy don't have any clue if server is online, by setting this to true proxy will see this server as online if plugin connected to proxy.")] 14 | public bool UseAccurateOnlineStatus { get; set; } = false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /XProxy/Models/BuildInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace XProxy.Models 4 | { 5 | public class BuildInfo 6 | { 7 | private Version _parsedVersion; 8 | 9 | [JsonIgnore] 10 | public Version ParsedVersion 11 | { 12 | get 13 | { 14 | if (_parsedVersion == null) 15 | System.Version.TryParse(Version, out _parsedVersion); 16 | 17 | return _parsedVersion; 18 | } 19 | } 20 | 21 | public string Version { get; set; } = "0.0.0"; 22 | public string[] SupportedGameVersions { get; set; } = new string[0]; 23 | public string[] Changelogs { get; set; } = new string[0]; 24 | public string CoreUrl { get; set; } 25 | public string DependenciesUrl { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XProxy.BuildListing/Models/BuildInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace XProxy.Models 4 | { 5 | public class BuildInfo 6 | { 7 | private Version _parsedVersion; 8 | 9 | [JsonIgnore] 10 | public Version ParsedVersion 11 | { 12 | get 13 | { 14 | if (_parsedVersion == null) 15 | System.Version.TryParse(Version, out _parsedVersion); 16 | 17 | return _parsedVersion; 18 | } 19 | } 20 | 21 | public string Version { get; set; } = "0.0.0"; 22 | public string[] SupportedGameVersions { get; set; } = new string[0]; 23 | public string[] Changelogs { get; set; } = new string[0]; 24 | public string CoreUrl { get; set; } 25 | public string DependenciesUrl { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XProxy.BuildRelease/Models/BuildInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace XProxy.Models 4 | { 5 | public class BuildInfo 6 | { 7 | private Version _parsedVersion; 8 | 9 | [JsonIgnore] 10 | public Version ParsedVersion 11 | { 12 | get 13 | { 14 | if (_parsedVersion == null) 15 | System.Version.TryParse(Version, out _parsedVersion); 16 | 17 | return _parsedVersion; 18 | } 19 | } 20 | 21 | public string Version { get; set; } = "0.0.0"; 22 | public string[] SupportedGameVersions { get; set; } = new string[0]; 23 | public string[] Changelogs { get; set; } = new string[0]; 24 | public string CoreUrl { get; set; } 25 | public string DependenciesUrl { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XProxy/Serialization/YamlParser.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization.NamingConventions; 2 | using YamlDotNet.Serialization; 3 | using YamlDotNet.Core; 4 | 5 | namespace XProxy.Serialization 6 | { 7 | public static class ConsoleYamlParser 8 | { 9 | public static ISerializer Serializer { get; } = new SerializerBuilder() 10 | .WithNamingConvention(CamelCaseNamingConvention.Instance) 11 | .DisableAliases() 12 | .IgnoreFields() 13 | .WithDefaultScalarStyle(ScalarStyle.SingleQuoted) 14 | .Build(); 15 | 16 | public static IDeserializer Deserializer { get; } = new DeserializerBuilder() 17 | .WithNamingConvention(CamelCaseNamingConvention.Instance) 18 | .IgnoreUnmatchedProperties() 19 | .IgnoreFields() 20 | .Build(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/BuildInformation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyVersion( 5 | "1.8.5" 6 | )] 7 | 8 | namespace XProxy.Core 9 | { 10 | public static class BuildInformation 11 | { 12 | static Version _buildVersion; 13 | 14 | public static Version Version 15 | { 16 | get 17 | { 18 | if (_buildVersion == null) 19 | _buildVersion = Initializer.CoreAssembly.GetName().Version; 20 | 21 | return _buildVersion; 22 | } 23 | } 24 | 25 | public static string VersionText => Version.ToString(3); 26 | 27 | public static string[] SupportedGameVersions = 28 | { 29 | "14.1.0", 30 | }; 31 | 32 | public static string[] Changelogs = new string[0]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /XProxy.BuildListing/XProxy.BuildListing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | Windows 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /XProxy.BuildRelease/XProxy.BuildRelease.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | enable 7 | Windows 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /XProxy.Core/Models/CustomNetLogger.cs: -------------------------------------------------------------------------------- 1 | using LiteNetLib; 2 | 3 | namespace XProxy.Core.Models 4 | { 5 | public class CustomNetLogger : INetLogger 6 | { 7 | private const string _tag = "LiteNetLib"; 8 | 9 | public void WriteNet(NetLogLevel level, string str, params object[] args) 10 | { 11 | string text = string.Format(str, args); 12 | switch (level) 13 | { 14 | case NetLogLevel.Error: 15 | Logger.Error(text, _tag); 16 | break; 17 | case NetLogLevel.Trace: 18 | case NetLogLevel.Warning: 19 | Logger.Warn(text, _tag); 20 | break; 21 | case NetLogLevel.Info: 22 | Logger.Info(text, _tag); 23 | break; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XProxy.Plugin/Config.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace XProxy.Plugin 4 | { 5 | public class Config 6 | { 7 | [Description("IP of proxy which will be used for making connection.")] 8 | public string ProxyIP { get; set; } = "127.0.0.1"; 9 | 10 | [Description("Port of proxy.")] 11 | public ushort ProxyPort { get; set; } = 7777; 12 | 13 | [Description("Connection key is used for secure connection and only allows connection with correct key.")] 14 | public string ConnectionKey { get; set; } = ""; 15 | 16 | [Description("Should plugin only allow player connections from proxy.")] 17 | public bool OnlyAllowProxyConnections { get; set; } = false; 18 | 19 | [Description("Rejection message when player is not connecting from proxy.")] 20 | public string RejectionMessage { get; set; } = "You are not connecting from proxy. Use %ProxyIP%:%ProxyPort% instead!"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /XProxy.Core/Serialization/CommentsObjectGraphVisitor.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Core; 2 | using YamlDotNet.Serialization.ObjectGraphVisitors; 3 | using YamlDotNet.Serialization; 4 | using YamlDotNet.Core.Events; 5 | 6 | namespace XProxy.Shared.Serialization 7 | { 8 | public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor 9 | { 10 | public CommentsObjectGraphVisitor(IObjectGraphVisitor nextVisitor) : base(nextVisitor) 11 | { 12 | } 13 | 14 | public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context, ObjectSerializer serializer) 15 | { 16 | CommentsObjectDescriptor commentsObjectDescriptor = value as CommentsObjectDescriptor; 17 | if (commentsObjectDescriptor != null && commentsObjectDescriptor.Comment != null) 18 | { 19 | context.Emit(new Comment(commentsObjectDescriptor.Comment, false)); 20 | } 21 | 22 | return base.EnterMapping(key, value, context, serializer); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /XProxy.Core/Services/CleanupService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using XProxy.Models; 9 | 10 | namespace XProxy.Core.Services 11 | { 12 | public class CleanupService : BackgroundService 13 | { 14 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 15 | { 16 | while (!stoppingToken.IsCancellationRequested) 17 | { 18 | try 19 | { 20 | foreach(var itemsToRemove in Listener.ForceServerForUserID.Where(x => x.Value.Time < DateTime.Now)) 21 | { 22 | Listener.ForceServerForUserID.Remove(itemsToRemove.Key, out LastServerInfo _); 23 | } 24 | } 25 | catch(Exception ex) 26 | { 27 | Logger.Error(ex); 28 | } 29 | await Task.Delay(1000); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Storage/egg-s-c-p--s-l-proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PANEL", 3 | "meta": { 4 | "version": "PTDL_v2", 5 | "update_url": null 6 | }, 7 | "exported_at": "2024-08-03T12:26:36+00:00", 8 | "name": "SCP: SL Proxy", 9 | "author": "Killers0992@gmail.com", 10 | "uuid": "0f6c32d8-0f15-4f9a-a8b3-3e78be5c88ac", 11 | "description": "Proxy for SCP: Secret Laboratory", 12 | "features": [], 13 | "docker_images": { 14 | "Dotnet 8": "ghcr.io\/parkervcp\/yolks:dotnet_8" 15 | }, 16 | "file_denylist": [], 17 | "startup": ".\/XProxy", 18 | "config": { 19 | "files": "{}", 20 | "startup": "{\n \"done\": \"Listening on server\"\n}", 21 | "logs": "{}", 22 | "stop": "^C" 23 | }, 24 | "scripts": { 25 | "installation": { 26 | "script": "#!\/bin\/ash\r\n\r\ncd \/mnt\/server\r\n\r\ncurl -sSL \"https:\/\/github.com\/Killers0992\/XProxy\/releases\/latest\/download\/XProxy\" -o XProxy\r\nchmod -R 777 .\/XProxy", 27 | "container": "ghcr.io\/parkervcp\/installers:alpine", 28 | "entrypoint": "ash" 29 | } 30 | }, 31 | "variables": [] 32 | } 33 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Events/Args/ProxyConnectionRequest.cs: -------------------------------------------------------------------------------- 1 | using LiteNetLib; 2 | using System; 3 | 4 | namespace XProxy.Core.Events.Args 5 | { 6 | public class ProxyConnectionRequest : BaseCancellableEvent 7 | { 8 | private ConnectionRequest _request; 9 | 10 | public ProxyConnectionRequest(Listener proxy, ConnectionRequest request, string ipAddress, string userId, CentralAuthPreauthFlags flags) 11 | { 12 | Proxy = proxy; 13 | 14 | _request = request; 15 | 16 | IpAddress = ipAddress; 17 | UserId = userId; 18 | Flags = flags; 19 | } 20 | 21 | public Listener Proxy { get; } 22 | public string IpAddress { get; } 23 | public string UserId { get; } 24 | public CentralAuthPreauthFlags Flags { get; } 25 | 26 | public void Disconnect() => _request.RejectForce(); 27 | public void Disconnect(string reason) => _request.Disconnect(reason); 28 | public void DisconnectBanned(string reason, long expiration) => _request.DisconnectBanned(reason, expiration); 29 | public void DisconnectBanned(string reason, DateTime date) => _request.DisconnectBanned(reason, date); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /XProxy.Core/Models/ScpServerList.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace XProxy.Models 5 | { 6 | public class ScpServerList 7 | { 8 | [Description("If you want to list your server on SCPSL ServerList set this to true.")] 9 | public bool UseScpServerList { get; set; } 10 | 11 | [Description("This IP is used for listing your server on SCPSL Server List ( auto = automatically gets public ip )")] 12 | public string AddressIp { get; set; } = "auto"; 13 | 14 | [Description("This NAME is used for listing your server on SCPSL Server List.")] 15 | [YamlMember(ScalarStyle = YamlDotNet.Core.ScalarStyle.Literal)] 16 | public string Name { get; set; } = 17 | @"Example server name. 18 | Proxy Server"; 19 | 20 | [Description("This PASTEBIN is used for server information on SCPSL Server List.")] 21 | public string Pastebin { get; set; } = "7wV681fT"; 22 | 23 | 24 | [Description("Setting this value for example to lobby it will take PlayerCount + MaxPlayerCount and it will use for displaying on serverlist.")] 25 | public string TakePlayerCountFromServer { get; set; } = string.Empty; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /XProxy.Core/Core/PluginT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using XProxy.Shared.Serialization; 4 | 5 | namespace XProxy.Core 6 | { 7 | public abstract class Plugin : Plugin where T : class, new() 8 | { 9 | string _configPath => Path.Combine(PluginDirectory, "config.yml"); 10 | 11 | public T Config { get; private set; } 12 | 13 | public override void LoadConfig() 14 | { 15 | if (!Directory.Exists(PluginDirectory)) 16 | Directory.CreateDirectory(PluginDirectory); 17 | 18 | if (!File.Exists(_configPath)) 19 | { 20 | File.WriteAllText(_configPath, YamlParser.Serializer.Serialize(Activator.CreateInstance(typeof(T)))); 21 | } 22 | 23 | string text = File.ReadAllText(_configPath); 24 | Config = YamlParser.Deserializer.Deserialize(text); 25 | SaveConfig(); 26 | } 27 | 28 | public override void SaveConfig() 29 | { 30 | if (!Directory.Exists(PluginDirectory)) 31 | Directory.CreateDirectory(PluginDirectory); 32 | 33 | File.WriteAllText(_configPath, YamlParser.Serializer.Serialize(Config)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /XProxy.Core/Serialization/YamlParser.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization.NamingConventions; 2 | using YamlDotNet.Serialization; 3 | using YamlDotNet.Core; 4 | 5 | namespace XProxy.Shared.Serialization 6 | { 7 | public static class YamlParser 8 | { 9 | public static ISerializer Serializer { get; } = new SerializerBuilder() 10 | .WithEmissionPhaseObjectGraphVisitor((EmissionPhaseObjectGraphVisitorArgs visitor) => 11 | new CommentsObjectGraphVisitor(visitor.InnerVisitor)) 12 | .WithTypeInspector((ITypeInspector typeInspector) => 13 | new CommentGatheringTypeInspector(typeInspector)) 14 | .WithNamingConvention(CamelCaseNamingConvention.Instance) 15 | .DisableAliases() 16 | .IgnoreFields() 17 | .WithDefaultScalarStyle(ScalarStyle.SingleQuoted) 18 | .Build(); 19 | 20 | public static IDeserializer Deserializer { get; } = new DeserializerBuilder() 21 | .WithNamingConvention(CamelCaseNamingConvention.Instance) 22 | .IgnoreUnmatchedProperties() 23 | .IgnoreFields() 24 | .Build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /XProxy.Plugin/XProxy.Plugin.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net48 5 | x64 6 | Library 7 | 8 | latest 9 | 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/listing.yml: -------------------------------------------------------------------------------- 1 | name: Build Listings 2 | 3 | env: 4 | websiteDirectory: Website 5 | 6 | on: 7 | workflow_dispatch: 8 | workflow_run: 9 | workflows: [Build Release] 10 | types: 11 | - completed 12 | release: 13 | types: [published, created, edited, unpublished, deleted, released] 14 | 15 | permissions: 16 | contents: read 17 | pages: write 18 | id-token: write 19 | 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | build-listing: 26 | name: build-listing 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout Main Repository. 33 | uses: actions/checkout@v2 34 | - name: Build listing 35 | id: build-listing 36 | uses: ./XProxy.BuildListing 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | - name: Setup Pages 40 | uses: actions/configure-pages@f156874f8191504dae5b037505266ed5dda6c382 41 | - name: Upload Pages Artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | path: ${{ env.websiteDirectory }} 45 | - name: Deploy to GitHub Pages 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /XProxy.Core/Services/ListenersService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using XProxy.Core; 6 | 7 | namespace XProxy.Services 8 | { 9 | public class ListenersService : BackgroundService 10 | { 11 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 12 | { 13 | foreach(var listener in ConfigService.Singleton.Value.Listeners) 14 | { 15 | new Listener(listener.Key, stoppingToken); 16 | } 17 | 18 | await RunServerUpdater(stoppingToken); 19 | } 20 | 21 | private async Task RunServerUpdater(CancellationToken token) 22 | { 23 | while (!token.IsCancellationRequested) 24 | { 25 | if (Server.UpdateServers) 26 | { 27 | try 28 | { 29 | Server.Refresh(false); 30 | } 31 | catch (Exception ex) 32 | { 33 | Logger.Error(ex); 34 | } 35 | Server.UpdateServers = false; 36 | } 37 | await Task.Delay(1000, token); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /XProxy.Core/Serialization/CommentsObjectDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using YamlDotNet.Core; 3 | using YamlDotNet.Serialization; 4 | 5 | namespace XProxy.Shared.Serialization 6 | { 7 | internal sealed class CommentsObjectDescriptor : IObjectDescriptor 8 | { 9 | public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment) 10 | { 11 | this._innerDescriptor = innerDescriptor; 12 | this.Comment = comment; 13 | } 14 | 15 | public string Comment { get; private set; } 16 | 17 | public object Value 18 | { 19 | get 20 | { 21 | return this._innerDescriptor.Value; 22 | } 23 | } 24 | 25 | public Type Type 26 | { 27 | get 28 | { 29 | return this._innerDescriptor.Type; 30 | } 31 | } 32 | 33 | public Type StaticType 34 | { 35 | get 36 | { 37 | return this._innerDescriptor.StaticType; 38 | } 39 | } 40 | 41 | public ScalarStyle ScalarStyle 42 | { 43 | get 44 | { 45 | return this._innerDescriptor.ScalarStyle; 46 | } 47 | } 48 | 49 | private readonly IObjectDescriptor _innerDescriptor; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /XProxy.Core/Monitors/CpuProfiler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace XProxy.Core.Monitors 10 | { 11 | public class CpuProfiler 12 | { 13 | public static double GetCpuUsage() 14 | { 15 | using (var process = Process.GetCurrentProcess()) 16 | { 17 | // Take initial CPU time snapshot 18 | TimeSpan startCpuTime = process.TotalProcessorTime; 19 | DateTime startTime = DateTime.UtcNow; 20 | 21 | // Sleep for an interval to measure CPU time 22 | Thread.Sleep(500); // Adjust as needed for better accuracy 23 | 24 | // Take a second CPU time snapshot 25 | TimeSpan endCpuTime = process.TotalProcessorTime; 26 | DateTime endTime = DateTime.UtcNow; 27 | 28 | // Calculate CPU usage 29 | double cpuUsedMs = (endCpuTime - startCpuTime).TotalMilliseconds; 30 | double elapsedMs = (endTime - startTime).TotalMilliseconds; 31 | 32 | // Percentage CPU = (CPU time used / elapsed time) * number of processors 33 | return (cpuUsedMs / elapsedMs) * 100 / Environment.ProcessorCount; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/ProxiedConnection.cs: -------------------------------------------------------------------------------- 1 | using LiteNetLib; 2 | using System; 3 | using XProxy.Services; 4 | 5 | namespace XProxy.Core.Connections 6 | { 7 | public class ProxiedConnection : BaseConnection 8 | { 9 | public ProxiedConnection(Player plr) : base(plr) 10 | { 11 | plr.InternalAcceptConnection(this); 12 | plr.ProcessMirrorMessagesFromProxy = true; 13 | plr.ProcessMirrorMessagesFromCurrentServer = true; 14 | } 15 | 16 | public override void OnConnected() 17 | { 18 | Logger.Info(ConfigService.Singleton.Messages.PlayerConnectedMessage.Replace("%tag%", Player.Tag).Replace("%address%", $"{Player.ClientEndPoint}").Replace("%userid%", Player.UserId), $"Player"); 19 | } 20 | 21 | public override void OnReceiveDataFromProxy(NetPacketReader reader, DeliveryMethod method) 22 | { 23 | try 24 | { 25 | Player.MainConnectionHandler.Send(reader.RawData, reader.Position, reader.AvailableBytes, method); 26 | } 27 | catch (Exception ex) 28 | { 29 | Logger.Error(ConfigService.Singleton.Messages.PlayerExceptionSendToServerMessage.Replace("%tag%", Player.ErrorTag).Replace("%address%", $"{Player.ClientEndPoint}").Replace("%userid%", Player.UserId).Replace("%message%", $"{ex}"), "Player"); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /XProxy.Core/XProxy.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | True 5 | net8.0 6 | AnyCPU;x64 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | $(SL_REFERENCES)\Assembly-CSharp.dll 22 | 23 | 24 | $(SL_REFERENCES)\BouncyCastle.Cryptography.dll 25 | 26 | 27 | $(SL_REFERENCES)\Mirror.dll 28 | 29 | 30 | $(SL_REFERENCES)\NorthwoodLib.dll 31 | 32 | 33 | $(UNITY_REFERENCES)\UnityEngine.CoreModule.dll 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /XProxy.Core/Serialization/CommentGatheringTypeInspector.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.Serialization.TypeInspectors; 2 | using YamlDotNet.Serialization; 3 | using System.Collections.Generic; 4 | using System; 5 | using System.Linq; 6 | using System.ComponentModel; 7 | 8 | namespace XProxy.Shared.Serialization 9 | { 10 | public class CommentGatheringTypeInspector : TypeInspectorSkeleton 11 | { 12 | public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor) 13 | { 14 | if (innerTypeDescriptor == null) 15 | { 16 | throw new ArgumentNullException("innerTypeDescriptor"); 17 | } 18 | this._innerTypeDescriptor = innerTypeDescriptor; 19 | } 20 | 21 | public override IEnumerable GetProperties(Type type, object container) 22 | { 23 | return from descriptor in this._innerTypeDescriptor.GetProperties(type, container) 24 | select new CommentsPropertyDescriptor(descriptor); 25 | } 26 | 27 | public override string GetEnumName(Type enumType, string name) 28 | { 29 | return this._innerTypeDescriptor.GetEnumName(enumType, name); 30 | } 31 | 32 | public override string GetEnumValue(object enumValue) 33 | { 34 | return this._innerTypeDescriptor.GetEnumValue(enumValue); 35 | } 36 | 37 | private readonly ITypeInspector _innerTypeDescriptor; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Events/EventManager.cs: -------------------------------------------------------------------------------- 1 | using XProxy.Core.Core.Events.Args; 2 | using XProxy.Core.Events.Args; 3 | using static XProxy.Core.Events.EventManager; 4 | 5 | namespace XProxy.Core.Events 6 | { 7 | public class EventManager 8 | { 9 | public delegate void CustomEventHandler(TEvent ev) 10 | where TEvent : BaseEvent; 11 | 12 | public static ProxyEvents Proxy { get; } = new ProxyEvents(); 13 | 14 | public static PlayerEvents Player { get; } = new PlayerEvents(); 15 | } 16 | 17 | public class ProxyEvents 18 | { 19 | public event CustomEventHandler ConnectionRequest; 20 | public void InvokeConnectionRequest(ProxyConnectionRequest ev) => ConnectionRequest?.InvokeWithExceptionHandler(ev); 21 | 22 | public event CustomEventHandler StartedListening; 23 | public void InvokeStartedListening(ProxyStartedListening ev) => StartedListening?.InvokeWithExceptionHandler(ev); 24 | } 25 | 26 | public class PlayerEvents 27 | { 28 | public event CustomEventHandler AssignTargetServer; 29 | public void InvokeAssignTargetServer(PlayerAssignTargetServer ev) => AssignTargetServer?.InvokeWithExceptionHandler(ev); 30 | 31 | public event CustomEventHandler CanJoin; 32 | public void InvokeCanJoin(PlayerCanJoinEvent ev) => CanJoin?.InvokeWithExceptionHandler(ev); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /XProxy.Core/Services/LoggingService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System; 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using XProxy.Shared; 7 | 8 | namespace XProxy.Services 9 | { 10 | public class LoggingService : BackgroundService 11 | { 12 | static void WriteLogToFile(object message) 13 | { 14 | if (!Directory.Exists("Logs")) 15 | Directory.CreateDirectory("Logs"); 16 | 17 | File.AppendAllLines($"Logs/log_{Logger.SessionTime.ToString("dd_MM_yyyy_hh_mm_ss")}.txt", new string[1] { message.ToString() }); 18 | } 19 | 20 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 21 | { 22 | while (!stoppingToken.IsCancellationRequested) 23 | { 24 | try 25 | { 26 | while(Logger.NewLogEntry.Count != 0) 27 | { 28 | if (Logger.NewLogEntry.TryDequeue(out string entry)) 29 | { 30 | WriteLogToFile(Logger.FormatAnsi(entry, true)); 31 | Console.WriteLine(Logger.FormatAnsi(entry)); 32 | } 33 | } 34 | } 35 | catch(Exception ex) 36 | { 37 | Logger.Error(ex, "XProxy"); 38 | } 39 | 40 | await Task.Delay(1000); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /XProxy.Core/Initializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using XProxy.Core.Services; 5 | using XProxy.Services; 6 | 7 | namespace XProxy 8 | { 9 | public class Initializer 10 | { 11 | static Assembly _coreAssembly; 12 | static PluginsService _plugins; 13 | 14 | public static Assembly CoreAssembly 15 | { 16 | get 17 | { 18 | if (_coreAssembly == null ) 19 | { 20 | _coreAssembly = typeof(Initializer).Assembly; 21 | } 22 | 23 | return _coreAssembly; 24 | } 25 | } 26 | 27 | public static void SetupServices(IServiceCollection services) 28 | { 29 | ConfigService.MainDirectory = Environment.CurrentDirectory; 30 | 31 | ConfigService.Singleton = new ConfigService(); 32 | 33 | _plugins = new PluginsService(services); 34 | 35 | CommandsService.RegisterConsoleCommandsInAssembly(Assembly.GetExecutingAssembly()); 36 | 37 | services.AddHostedService(); 38 | services.AddHostedService(); 39 | services.AddHostedService(); 40 | services.AddHostedService(); 41 | services.AddHostedService(); 42 | services.AddHostedService(); 43 | services.AddHostedService(); 44 | services.AddHostedService(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/BaseConnection.cs: -------------------------------------------------------------------------------- 1 | using LiteNetLib; 2 | using Mirror; 3 | using System; 4 | using XProxy.Core.Core.Connections.Responses; 5 | 6 | namespace XProxy.Core.Connections 7 | { 8 | public class BaseConnection : IDisposable 9 | { 10 | public Player Player { get; private set; } 11 | 12 | public BaseConnection(Player plr) 13 | { 14 | Player = plr; 15 | } 16 | 17 | internal void InternalConnected() 18 | { 19 | try 20 | { 21 | OnConnected(); 22 | } 23 | catch(Exception ex) 24 | { 25 | Logger.Error(ex, "Connection"); 26 | } 27 | } 28 | 29 | public virtual void OnReceiveGameConsoleCommand(string command, string[] args) 30 | { 31 | 32 | } 33 | 34 | public virtual void OnReceiveDataFromProxy(NetPacketReader reader, DeliveryMethod method) 35 | { 36 | 37 | } 38 | 39 | public virtual void OnReceiveMirrorDataFromProxy(uint key, NetworkReader reader) 40 | { 41 | 42 | } 43 | 44 | public virtual void OnConnectionResponse(Server server, BaseResponse response) 45 | { 46 | 47 | } 48 | 49 | public virtual void OnConnected() 50 | { 51 | 52 | } 53 | 54 | public virtual void OnClientReady() 55 | { 56 | 57 | } 58 | 59 | public virtual void OnAddPlayer() 60 | { 61 | 62 | } 63 | 64 | public virtual void OnPlayerSpawned() 65 | { 66 | 67 | } 68 | 69 | public virtual void Update() 70 | { 71 | 72 | } 73 | 74 | public virtual void Dispose() 75 | { 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /XProxy.Core/Services/QueueService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace XProxy.Core.Services 7 | { 8 | public class QueueService : BackgroundService 9 | { 10 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 11 | { 12 | while (!stoppingToken.IsCancellationRequested) 13 | { 14 | try 15 | { 16 | foreach (var server in Server.List) 17 | { 18 | foreach(var ticket in server.PlayersInQueue) 19 | { 20 | if (!ticket.Value.IsTicketExpired()) 21 | continue; 22 | 23 | if (ticket.Value.IsConnecting) 24 | { 25 | Logger.Info($"Player {ticket.Key} joined server {server.Name} from queue successfully! ( freed slot )", "QueueService"); 26 | } 27 | else 28 | { 29 | Logger.Info($"Queue slot for {ticket.Key} expired! ( server {server.Name} )", "QueueService"); 30 | } 31 | 32 | server.PlayersInQueueByUserId.Remove(ticket.Value.UserId); 33 | server.PlayersInQueue.TryRemove(ticket.Key, out _); 34 | } 35 | } 36 | } 37 | catch (Exception ex) 38 | { 39 | Logger.Error(ex, "QueueService"); 40 | } 41 | 42 | await Task.Delay(1000); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/PlaceHolders.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace XProxy.Core 4 | { 5 | public class PlaceHolders 6 | { 7 | public static string ReplacePlaceholders(string text) 8 | { 9 | return Regex.Replace(text, @"%(\w+?)(?:_(\w+))?%", match => 10 | { 11 | string placeholderType = match.Groups[1].Value.ToLower(); 12 | string serverName = match.Groups[2].Success ? match.Groups[2].Value : null; 13 | 14 | Server.TryGetByName(serverName, out Server targetServer); 15 | 16 | switch (placeholderType.ToLower()) 17 | { 18 | case "playersinqueue": 19 | if (targetServer == null) 20 | return "-1"; 21 | 22 | return $"{targetServer.PlayersInQueueCount}"; 23 | case "onlineplayers": 24 | if (targetServer == null) 25 | return "-1"; 26 | 27 | return $"{targetServer.PlayersCount}"; 28 | case "maxplayers": 29 | if (targetServer == null) 30 | return "-1"; 31 | 32 | return $"{targetServer.PlayersCount}"; 33 | case "proxyonlineplayers": 34 | return Player.Count.ToString(); 35 | case "proxymaxplayers": 36 | if (!Listener.TryGet(serverName, out Listener list)) 37 | return "-1"; 38 | 39 | return list.Settings.MaxPlayers.ToString(); 40 | default: 41 | return "%placeholder_not_found%"; 42 | } 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/ConnectionValidator.cs: -------------------------------------------------------------------------------- 1 | using LiteNetLib; 2 | 3 | namespace XProxy.Core.Core.Connections 4 | { 5 | public class ConnectionValidator 6 | { 7 | public ConnectionHandler Parent; 8 | 9 | public ConnectionValidator(ConnectionHandler parent) 10 | { 11 | Parent = parent; 12 | } 13 | 14 | public int ClientChallengeId; 15 | 16 | public ushort ClientChallengeSecretLen; 17 | public byte[] ClientChallenge, ClientChallengeBase, ClientChallengeResponse; 18 | 19 | public void ProcessChallenge(NetPacketReader reader) 20 | { 21 | if (!reader.TryGetByte(out byte mode) || !reader.TryGetInt(out ClientChallengeId)) 22 | return; 23 | 24 | ChallengeType challengeType = (ChallengeType)mode; 25 | 26 | switch (challengeType) 27 | { 28 | case ChallengeType.Reply: 29 | if (reader.TryGetBytesWithLength(out ClientChallengeResponse)) 30 | { 31 | Parent.Reconnect(Parent.Owner.PreAuth.CreateChallenge(ClientChallengeId, ClientChallengeResponse, Parent.Server.Settings.SendIpAddressInPreAuth)); 32 | } 33 | break; 34 | 35 | case ChallengeType.MD5: 36 | case ChallengeType.SHA1: 37 | if (reader.TryGetBytesWithLength(out ClientChallengeBase) && 38 | reader.TryGetUShort(out ClientChallengeSecretLen) && 39 | reader.TryGetBytesWithLength(out ClientChallenge)) 40 | { 41 | Logger.Info($"Received challenge {challengeType} which is not supported"); 42 | } 43 | break; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /XProxy/Models/LauncherSettings.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using XProxy.Misc; 3 | using XProxy.Serialization; 4 | 5 | namespace XProxy.Models 6 | { 7 | public class LauncherSettings 8 | { 9 | public const string LauncherPath = "./config.yml"; 10 | public const int MaxRetries = 5; 11 | 12 | public static LauncherSettings Value; 13 | 14 | public static bool Load() 15 | { 16 | int retryCount = 0; 17 | 18 | retryLoading: 19 | 20 | if (!File.Exists(LauncherPath)) 21 | { 22 | File.WriteAllText(LauncherPath, ConsoleYamlParser.Serializer.Serialize(new LauncherSettings())); 23 | } 24 | 25 | try 26 | { 27 | string content = File.ReadAllText(LauncherPath); 28 | Value = ConsoleYamlParser.Deserializer.Deserialize(content); 29 | } 30 | catch (Exception ex) 31 | { 32 | ConsoleLogger.Error("Failed loading config \"config.yml\", press any key to retry"); 33 | ConsoleLogger.Error(ex); 34 | 35 | Console.ReadKey(); 36 | retryCount++; 37 | 38 | if (retryCount >= MaxRetries) 39 | return false; 40 | 41 | goto retryLoading; 42 | } 43 | 44 | ConsoleLogger.AnsiDisabled = Value.DisableAnsiColors; 45 | 46 | return true; 47 | } 48 | 49 | [Description("Disables ansi colors in console logs")] 50 | public bool DisableAnsiColors { get; set; } = false; 51 | 52 | [Description("If set to true updater will not try to update XProxy on startup.")] 53 | public bool DisableUpdater { get; set; } = false; 54 | 55 | [Description("For which version of game XProxy will be downloaded")] 56 | public string GameVersion { get; set; } = "latest"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/CustomUnbatcher.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | public class CustomUnbatcher 6 | { 7 | readonly Queue batches = new Queue(); 8 | 9 | public int BatchesCount => batches.Count; 10 | 11 | readonly NetworkReader reader = new NetworkReader(new byte[0]); 12 | 13 | double readerRemoteTimeStamp; 14 | 15 | void StartReadingBatch(NetworkWriter batch) 16 | { 17 | reader.SetBuffer(batch.ToArraySegment()); 18 | 19 | readerRemoteTimeStamp = reader.ReadDouble(); 20 | } 21 | 22 | public bool AddBatch(ArraySegment batch) 23 | { 24 | if (batch.Count < Batcher.TimestampSize) 25 | return false; 26 | 27 | NetworkWriter writer = new NetworkWriter(); 28 | writer.WriteBytes(batch.Array, batch.Offset, batch.Count); 29 | 30 | if (batches.Count == 0) 31 | StartReadingBatch(writer); 32 | 33 | batches.Enqueue(writer); 34 | return true; 35 | } 36 | 37 | public bool GetNextMessage(out ArraySegment message, out double remoteTimeStamp) 38 | { 39 | message = default; 40 | remoteTimeStamp = 0; 41 | 42 | if (batches.Count == 0) 43 | return false; 44 | 45 | if (reader.Capacity == 0) 46 | return false; 47 | 48 | if (reader.Remaining == 0) 49 | { 50 | batches.Dequeue(); 51 | 52 | if (batches.Count > 0) 53 | { 54 | NetworkWriter next = batches.Peek(); 55 | StartReadingBatch(next); 56 | } 57 | else return false; 58 | } 59 | 60 | remoteTimeStamp = readerRemoteTimeStamp; 61 | 62 | if (reader.Remaining == 0) 63 | return false; 64 | 65 | int size = (int)Compression.DecompressVarUInt(reader); 66 | 67 | if (reader.Remaining < size) 68 | return false; 69 | 70 | message = reader.ReadBytesSegment(size); 71 | return true; 72 | } 73 | } -------------------------------------------------------------------------------- /XProxy.Core/Models/QueueTicket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace XProxy.Core.Models 4 | { 5 | public class QueueTicket 6 | { 7 | bool? _lastOfflineStatus = false; 8 | DateTime _offlineFor = DateTime.Now; 9 | Server _info; 10 | 11 | public QueueTicket(string userId, Server server) 12 | { 13 | UserId = userId; 14 | _info = server; 15 | } 16 | 17 | public string UserId { get; private set; } 18 | public int Position => _info.PlayersInQueueByUserId.IndexOf(UserId) + 1; 19 | public bool IsConnecting { get; private set; } 20 | public DateTime TicketLifetime { get; private set; } 21 | 22 | public bool IsPlayerOffline => !Listener.ConnectionToUserId.ContainsKey(UserId); 23 | public TimeSpan OfflineTime => DateTime.Now - _offlineFor; 24 | 25 | public bool IsPlayerConnected 26 | { 27 | get 28 | { 29 | if (Player.TryGet(UserId, out Player plr)) 30 | return false; 31 | 32 | return plr.IsConnectedToCurrentServer; 33 | } 34 | } 35 | 36 | public bool IsTicketExpired() 37 | { 38 | if (IsConnecting) 39 | return TicketLifetime < DateTime.Now; 40 | 41 | if (IsPlayerOffline) 42 | { 43 | if (_lastOfflineStatus.HasValue) 44 | { 45 | return OfflineTime.TotalSeconds > 5; 46 | } 47 | else 48 | { 49 | _offlineFor = DateTime.Now; 50 | _lastOfflineStatus = true; 51 | return false; 52 | } 53 | } 54 | else if (IsPlayerConnected) 55 | return true; 56 | else 57 | _lastOfflineStatus = null; 58 | 59 | return false; 60 | } 61 | 62 | public void MarkAsConnecting() 63 | { 64 | IsConnecting = true; 65 | TicketLifetime = DateTime.Now.AddSeconds(15); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/CustomBatcher.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | public class CustomBatcher 6 | { 7 | readonly int threshold; 8 | 9 | public const int TimestampSize = sizeof(double); 10 | 11 | public static int MessageHeaderSize(int messageSize) => 12 | Compression.VarUIntSize((ulong)messageSize); 13 | 14 | public static int MaxMessageOverhead(int messageSize) => 15 | TimestampSize + MessageHeaderSize(messageSize); 16 | 17 | readonly Queue batches = new Queue(); 18 | 19 | NetworkWriter batch; 20 | 21 | public CustomBatcher(int threshold) 22 | { 23 | this.threshold = threshold; 24 | } 25 | 26 | public void AddMessage(ArraySegment message, double timeStamp) 27 | { 28 | if (message == null) 29 | return; 30 | 31 | int headerSize = Compression.VarUIntSize((ulong)message.Count); 32 | int neededSize = headerSize + message.Count; 33 | 34 | if (batch != null && 35 | batch.Position + neededSize > threshold) 36 | { 37 | batches.Enqueue(batch); 38 | batch = null; 39 | } 40 | 41 | if (batch == null) 42 | { 43 | batch = new NetworkWriter(); 44 | batch.WriteDouble(timeStamp); 45 | } 46 | 47 | Compression.CompressVarUInt(batch, (ulong)message.Count); 48 | batch.WriteBytes(message.Array, message.Offset, message.Count); 49 | } 50 | 51 | static void CopyAndReturn(NetworkWriter batch, NetworkWriter writer) 52 | { 53 | if (writer.Position != 0) 54 | throw new ArgumentException($"GetBatch needs a fresh writer!"); 55 | 56 | ArraySegment segment = batch.ToArraySegment(); 57 | writer.WriteBytes(segment.Array, segment.Offset, segment.Count); 58 | } 59 | 60 | public bool GetBatch(NetworkWriter writer) 61 | { 62 | if (batches.TryDequeue(out NetworkWriter first)) 63 | { 64 | CopyAndReturn(first, writer); 65 | return true; 66 | } 67 | 68 | if (batch != null) 69 | { 70 | CopyAndReturn(batch, writer); 71 | batch = null; 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | } -------------------------------------------------------------------------------- /XProxy/Misc/Extensions.cs: -------------------------------------------------------------------------------- 1 | public static class Extensions 2 | { 3 | public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress progress = null, CancellationToken cancellationToken = default) 4 | { 5 | using (var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead)) 6 | { 7 | var contentLength = response.Content.Headers.ContentLength; 8 | 9 | using (var download = await response.Content.ReadAsStreamAsync(cancellationToken)) 10 | { 11 | if (progress == null || !contentLength.HasValue) 12 | { 13 | await download.CopyToAsync(destination); 14 | return; 15 | } 16 | 17 | var relativeProgress = new Progress(totalBytes => progress.Report((float)totalBytes / contentLength.Value)); 18 | await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken); 19 | progress.Report(1); 20 | } 21 | } 22 | } 23 | 24 | public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress = null, CancellationToken cancellationToken = default) 25 | { 26 | if (source == null) 27 | throw new ArgumentNullException(nameof(source)); 28 | if (!source.CanRead) 29 | throw new ArgumentException("Has to be readable", nameof(source)); 30 | if (destination == null) 31 | throw new ArgumentNullException(nameof(destination)); 32 | if (!destination.CanWrite) 33 | throw new ArgumentException("Has to be writable", nameof(destination)); 34 | if (bufferSize < 0) 35 | throw new ArgumentOutOfRangeException(nameof(bufferSize)); 36 | 37 | var buffer = new byte[bufferSize]; 38 | long totalBytesRead = 0; 39 | int bytesRead; 40 | while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) 41 | { 42 | await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); 43 | totalBytesRead += bytesRead; 44 | progress?.Report(totalBytesRead); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/LostConnection.cs: -------------------------------------------------------------------------------- 1 | using XProxy.Enums; 2 | using XProxy.Services; 3 | 4 | namespace XProxy.Core.Connections 5 | { 6 | public class LostConnection : SimulatedConnection 7 | { 8 | bool _searching; 9 | bool _disconnecting; 10 | int _timer = 1; 11 | bool _foundNextServer; 12 | 13 | public LostConnectionType LostConnectionType { get; private set; } 14 | public int LostConnectionTime { get; private set; } 15 | 16 | public LostConnection(Player plr, LostConnectionType type) : base(plr) 17 | { 18 | LostConnectionType = type; 19 | } 20 | 21 | public override void Update() 22 | { 23 | if (LostConnectionTime != 2) 24 | { 25 | LostConnectionTime++; 26 | Player.SendHint(ConfigService.Singleton.Messages.LostConnectionHint.Replace("%time%", $"{LostConnectionTime}"), 1); 27 | return; 28 | } 29 | 30 | if (_timer != 0 && !_searching) 31 | { 32 | Player.SendHint(ConfigService.Singleton.Messages.SearchingForFallbackServerHint, 1); 33 | _timer--; 34 | return; 35 | } 36 | 37 | _searching = true; 38 | if (_disconnecting) 39 | { 40 | if (_timer == 0) 41 | { 42 | if (_foundNextServer) 43 | Player.Roundrestart(1); 44 | else 45 | Player.DisconnectFromProxy(); 46 | } 47 | else 48 | { 49 | _timer--; 50 | } 51 | } 52 | else 53 | { 54 | Server fallback = Player.CurrentServer.GetFallbackServer(Player); 55 | 56 | if (fallback == null) 57 | { 58 | Player.SendHint(ConfigService.Singleton.Messages.OnlineServerNotFoundHint, 4); 59 | } 60 | else 61 | { 62 | _foundNextServer = true; 63 | Player.SaveServerForNextSession(fallback.Name, 7f); 64 | Player.SendHint(ConfigService.Singleton.Messages.ConnectingToServerHint.Replace("%server%", fallback.Name), 4); 65 | } 66 | 67 | _disconnecting = true; 68 | _timer = 2; 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /XProxy.Core/Models/ServerModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using XProxy.Enums; 8 | using YamlDotNet.Serialization; 9 | 10 | namespace XProxy.Core.Models 11 | { 12 | public class ServerModel 13 | { 14 | [Description("Name of server.")] 15 | [YamlMember(ScalarStyle = YamlDotNet.Core.ScalarStyle.Literal)] 16 | public string Name { get; set; } 17 | 18 | [Description("IP Address of target server.")] 19 | public string Ip { get; set; } = "0.0.0.0"; 20 | 21 | [Description("IP Address used for connecting to server via providing public ip in .connect command ( for GLOBAL MODERATION, STUDIO STAFF )")] 22 | public string PublicIp { get; set; } = "0.0.0.0"; 23 | 24 | [Description("Port of target server.")] 25 | public ushort Port { get; set; } 26 | 27 | [Description("Maximum amount of players which can connect to server.")] 28 | public int MaxPlayers { get; set; } = 20; 29 | 30 | [Description("Enables queue system for this server.")] 31 | public bool IsQueueEnabled { get; set; } = false; 32 | 33 | [Description("Maximum amount of players which can be in queue for this server.")] 34 | public int QueueSlots { get; set; } = 50; 35 | 36 | [Description("Connection type set to Proxied will connect players to specific server, simulation needs to have Simulation set to specific type example lobby")] 37 | public ConnectionType ConnectionType { get; set; } = ConnectionType.Proxied; 38 | [Description("Simulation set when player connects to server, plugins can register custom ones and you need to specify type here.")] 39 | public string Simulation { get; set; } = "-"; 40 | 41 | [Description("PreAuth will contain IP Address of client and target server will set this ip address to that one only if enable_proxy_ip_passthrough is set to true and trusted_proxies_ip_addresses has your proxy ip!")] 42 | public bool SendIpAddressInPreAuth { get; set; } = true; 43 | 44 | [Description("Fallback servers if defined are used for connecting players to online server if current server crashes or shutdowns.")] 45 | public List FallbackServers { get; set; } = new List() { "lobby" }; 46 | 47 | [Description("These settings are related for XProxy.Plugin running on your server.")] 48 | public PluginExtensionModel PluginExtension { get; set; } = new PluginExtensionModel(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /XProxy.Core/Serialization/CommentsPropertyDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using YamlDotNet.Core; 4 | using YamlDotNet.Serialization; 5 | 6 | namespace XProxy.Shared.Serialization 7 | { 8 | internal sealed class CommentsPropertyDescriptor : IPropertyDescriptor 9 | { 10 | public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor) 11 | { 12 | this._baseDescriptor = baseDescriptor; 13 | this.Name = baseDescriptor.Name; 14 | } 15 | 16 | public string Name { get; set; } 17 | 18 | public Type Type 19 | { 20 | get 21 | { 22 | return this._baseDescriptor.Type; 23 | } 24 | } 25 | 26 | public Type TypeOverride 27 | { 28 | get 29 | { 30 | return this._baseDescriptor.TypeOverride; 31 | } 32 | set 33 | { 34 | this._baseDescriptor.TypeOverride = value; 35 | } 36 | } 37 | 38 | public int Order { get; set; } 39 | 40 | public ScalarStyle ScalarStyle 41 | { 42 | get 43 | { 44 | return this._baseDescriptor.ScalarStyle; 45 | } 46 | set 47 | { 48 | this._baseDescriptor.ScalarStyle = value; 49 | } 50 | } 51 | 52 | public bool CanWrite 53 | { 54 | get 55 | { 56 | return this._baseDescriptor.CanWrite; 57 | } 58 | } 59 | 60 | public bool AllowNulls => _baseDescriptor.AllowNulls; 61 | 62 | public bool Required => _baseDescriptor.Required; 63 | 64 | public Type ConverterType => _baseDescriptor.ConverterType; 65 | 66 | public void Write(object target, object value) 67 | { 68 | this._baseDescriptor.Write(target, value); 69 | } 70 | 71 | public T GetCustomAttribute() where T : Attribute 72 | { 73 | return this._baseDescriptor.GetCustomAttribute(); 74 | } 75 | 76 | public IObjectDescriptor Read(object target) 77 | { 78 | DescriptionAttribute customAttribute = this._baseDescriptor.GetCustomAttribute(); 79 | if (customAttribute == null) 80 | { 81 | return this._baseDescriptor.Read(target); 82 | } 83 | return new CommentsObjectDescriptor(this._baseDescriptor.Read(target), customAttribute.Description); 84 | } 85 | 86 | private readonly IPropertyDescriptor _baseDescriptor; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/HintBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace XProxy.Core 5 | { 6 | public class HintBuilder 7 | { 8 | public class HintLine 9 | { 10 | public string Left = string.Empty; 11 | public string Center = string.Empty; 12 | public string Right = string.Empty; 13 | 14 | public bool AllEmpty => string.IsNullOrEmpty(Left) && string.IsNullOrEmpty(Center) && string.IsNullOrEmpty(Right); 15 | } 16 | 17 | public HintBuilder() 18 | { 19 | for (int x = 0; x < 33; x++) 20 | { 21 | Lines.Add(new HintLine()); 22 | } 23 | } 24 | 25 | public List Lines = new List(); 26 | 27 | public void SetRightLine(int line, string text) 28 | { 29 | Lines[line - 1].Right = text; 30 | } 31 | 32 | public void SetLeftLine(int line, string text) 33 | { 34 | Lines[line - 1].Left = text; 35 | } 36 | 37 | public void SetCenterLine(int line, string text) 38 | { 39 | Lines[line - 1].Center = text; 40 | } 41 | 42 | public string Build() 43 | { 44 | StringBuilder builder = new StringBuilder(); 45 | 46 | for (int x = 0; x < Lines.Count; x++) 47 | { 48 | HintLine line = Lines[x]; 49 | 50 | if (line.AllEmpty) 51 | { 52 | builder.AppendLine(string.Empty); 53 | continue; 54 | } 55 | 56 | if (!string.IsNullOrEmpty(line.Center)) 57 | { 58 | builder.AppendLine($"{line.Center}"); 59 | continue; 60 | } 61 | 62 | if (!string.IsNullOrEmpty(line.Right) && string.IsNullOrEmpty(line.Left)) 63 | { 64 | builder.AppendLine($"{line.Right}"); 65 | continue; 66 | } 67 | 68 | if (string.IsNullOrEmpty(line.Right) && !string.IsNullOrEmpty(line.Left)) 69 | { 70 | builder.AppendLine($"{line.Left}"); 71 | continue; 72 | } 73 | 74 | if (!string.IsNullOrEmpty(line.Right) && !string.IsNullOrEmpty(line.Left)) 75 | { 76 | builder.AppendLine($"{line.Left}"); 77 | builder.AppendLine($"{line.Right}"); 78 | continue; 79 | } 80 | } 81 | 82 | return builder.ToString(); 83 | } 84 | 85 | public override string ToString() => Build(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | SL_REFERENCES: "${{ github.workspace }}/References/SCPSL_Data/Managed" 8 | UNITY_REFERENCES: "${{ github.workspace }}/References/SCPSL_Data/Managed" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | env: 16 | releasePath: "${{ github.workspace }}/main/XProxy.Shared" 17 | steps: 18 | - name: Checkout main repository. 19 | uses: actions/checkout@v4 20 | with: 21 | path: main 22 | - name: Download SCP SL References 23 | uses: killers0992/scpsl.downloadfiles@master 24 | with: 25 | branch: 'labapi-beta' 26 | filesToDownload: 'Assembly-CSharp.dll,Mirror.dll,BouncyCastle.Cryptography.dll,NorthwoodLib.dll,UnityEngine.CoreModule.dll,LabApi.dll' 27 | - name: Setup .NET 28 | uses: actions/setup-dotnet@v2 29 | with: 30 | dotnet-version: 9.0.x 31 | - name: Publish Linux x64 32 | run: dotnet publish ${{ github.workspace }}/main/XProxy/XProxy.csproj --configuration Release --self-contained false -p:PublishSingleFile=true --runtime linux-x64 33 | - name: Publish Windows x64 34 | run: dotnet publish ${{ github.workspace }}/main/XProxy/XProxy.csproj --configuration Release --self-contained false -p:PublishSingleFile=true --runtime win-x64 35 | - name: Publish Core 36 | run: dotnet publish ${{ github.workspace }}/main/XProxy.Core/XProxy.Core.csproj --configuration Release --self-contained --runtime win-x64 37 | - name: Build release 38 | id: build-release 39 | uses: ./main/XProxy.BuildRelease 40 | - name: Get Release Info 41 | id: release-info 42 | uses: zoexx/github-action-json-file-properties@b9f36ce6ee6fe2680cd3c32b2c62e22eade7e590 43 | with: 44 | file_path: "${{ github.workspace }}/main/releaseinfo.json" 45 | - name: Set Environment Variables 46 | run: | 47 | echo "version=${{ steps.release-info.outputs.version }}" >> $GITHUB_ENV 48 | - name: Create Tag 49 | id: tag_version 50 | uses: mathieudutour/github-tag-action@v6.1 51 | with: 52 | github_token: "${{ secrets.GITHUB_TOKEN }}" 53 | tag_prefix: "" 54 | custom_tag: "${{ env.version }}" 55 | - name: Make Release 56 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 57 | with: 58 | files: | 59 | ${{ github.workspace }}/main/XProxy/bin/Release/net8.0/linux-x64/publish/XProxy 60 | ${{ github.workspace }}/main/XProxy/bin/Release/net8.0/win-x64/publish/XProxy.exe 61 | ${{ github.workspace }}/main/XProxy.Core/bin/Release/net8.0/win-x64/publish/XProxy.Core.dll 62 | ${{ github.workspace }}/main/dependencies.zip 63 | ${{ github.workspace }}/main/releaseinfo.json 64 | tag_name: ${{ env.version }} 65 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/QueueConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using XProxy.Enums; 3 | using XProxy.Services; 4 | 5 | namespace XProxy.Core.Connections 6 | { 7 | public class QueueConnection : SimulatedConnection 8 | { 9 | public int NextAttempt = 5; 10 | 11 | public QueueConnection(Player plr) : base(plr) 12 | { 13 | } 14 | 15 | public override void OnConnected() 16 | { 17 | Player.SendToScene("Facility"); 18 | } 19 | 20 | public override void OnClientReady() 21 | { 22 | Player.ObjectSpawnStarted(); 23 | Player.ObjectSpawnFinished(); 24 | } 25 | 26 | public override void OnAddPlayer() 27 | { 28 | Player.Spawn(); 29 | } 30 | 31 | public override void Update() 32 | { 33 | int pos = Player.PositionInQueue; 34 | 35 | if (!Player.IsInQueue) 36 | { 37 | Player.JoinQueue(); 38 | return; 39 | } 40 | 41 | if (pos == 1) 42 | { 43 | if (NextAttempt == 0) 44 | { 45 | Player.ConnectTo(Player.CurrentServer); 46 | NextAttempt = 5; 47 | } 48 | else 49 | NextAttempt--; 50 | 51 | Player.SendHint(ConfigService.Singleton.Messages.FirstPositionInQueue.Replace("%position%", $"{pos}").Replace("%totalInQueue%", $"{Player.CurrentServer.PlayersInQueueCount}"), 1); 52 | } 53 | else 54 | { 55 | Player.SendHint(ConfigService.Singleton.Messages.PositionInQueue.Replace("%position%", $"{pos}").Replace("%totalInQueue%", $"{Player.CurrentServer.PlayersInQueueCount}"), 1); 56 | } 57 | } 58 | 59 | public override void OnReceiveGameConsoleCommand(string command, string[] args) 60 | { 61 | switch (command.ToLower()) 62 | { 63 | case "hub": 64 | case "lobby": 65 | var rng = Player.Proxy.GetRandomServerFromPriorities(); 66 | 67 | if (rng.Settings.ConnectionType == ConnectionType.Simulated) 68 | { 69 | if (Listener.Simulations.TryGetValue(rng.Settings.Simulation, out Type simType)) 70 | { 71 | Player.CurrentServer = rng; 72 | Player.Connection = (SimulatedConnection)Activator.CreateInstance(simType, args: Player); 73 | Player.SendGameConsoleMessage("Switch to lobby..."); 74 | } 75 | } 76 | else 77 | { 78 | Player.SendGameConsoleMessage("Connecting to lobby..."); 79 | } 80 | break; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /XProxy.Plugin/MainClass.cs: -------------------------------------------------------------------------------- 1 | using LabApi.Events.Arguments.PlayerEvents; 2 | using LabApi.Events.Arguments.ServerEvents; 3 | using LabApi.Events.Handlers; 4 | using LabApi.Features; 5 | using LabApi.Features.Console; 6 | using LabApi.Loader.Features.Plugins; 7 | using System; 8 | using XProxy.Core.Enums; 9 | using XProxy.Plugin; 10 | using XProxy.Plugin.Core; 11 | 12 | public class MainClass : Plugin 13 | { 14 | public static MainClass Singleton; 15 | 16 | public static ServerStatus Status; 17 | 18 | public override string Name { get; } = "XProxy.Plugin"; 19 | 20 | public override string Description { get; } = "Plugin made for connecting to proxy making better exchange of information between server and proxy."; 21 | 22 | public override string Author { get; } = "Killers0992"; 23 | 24 | public override Version Version { get; } = new Version(1, 0, 0); 25 | 26 | public override Version RequiredApiVersion { get; } = new Version(LabApiProperties.CompiledVersion); 27 | 28 | public override void Enable() 29 | { 30 | Singleton = this; 31 | UnityEngine.Object.FindObjectOfType().gameObject.AddComponent(); 32 | 33 | ServerEvents.WaitingForPlayers += OnWaitingForPlayers; 34 | ServerEvents.RoundStarted += OnRoundStarted; 35 | ServerEvents.RoundEnded += OnRoundEnded; 36 | ServerEvents.RoundRestarted += OnRoundRestarted; 37 | PlayerEvents.PreAuthenticating += OnPreAuth; 38 | } 39 | 40 | private void OnPreAuth(PlayerPreAuthenticatingEventArgs ev) 41 | { 42 | Logger.Info($"Player {ev?.UserId} is trying to connect from {ev?.IpAddress}."); 43 | Logger.Info($"ProxyIP: {Singleton.Config.ProxyIP}"); // Config.ProxyIP doesn't work for some reason 44 | Logger.Info($"OnlyAllowProxyConnections: {Singleton.Config.OnlyAllowProxyConnections}"); 45 | 46 | if (ev.IpAddress != Singleton.Config.ProxyIP && Singleton.Config.OnlyAllowProxyConnections) 47 | { 48 | string proxyRejectionMessage = Singleton.Config.RejectionMessage.Replace("%ProxyIP%", Singleton.Config.ProxyIP).Replace("%ProxyPort%", Singleton.Config.ProxyPort.ToString()); 49 | 50 | Logger.Info($"Player tried to connect from {ev.IpAddress} which is not proxy IP."); 51 | ev.RejectCustom(proxyRejectionMessage); 52 | return; 53 | } 54 | } 55 | 56 | private void OnRoundRestarted() => Status = ServerStatus.RoundRestart; 57 | 58 | private void OnRoundEnded(RoundEndedEventArgs ev) => Status = ServerStatus.RoundEnding; 59 | 60 | private void OnRoundStarted() => Status = ServerStatus.RoundInProgress; 61 | 62 | private void OnWaitingForPlayers() => Status = ServerStatus.WaitingForPlayers; 63 | 64 | public override void Disable() 65 | { 66 | ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; 67 | ServerEvents.RoundStarted -= OnRoundStarted; 68 | ServerEvents.RoundEnded -= OnRoundEnded; 69 | ServerEvents.RoundRestarted -= OnRoundRestarted; 70 | PlayerEvents.PreAuthenticating -= OnPreAuth; 71 | } 72 | } -------------------------------------------------------------------------------- /XProxy.Plugin/Core/ProxyConnection.cs: -------------------------------------------------------------------------------- 1 | using LabApi.Features.Console; 2 | using LabApi.Features.Wrappers; 3 | using LiteNetLib; 4 | using LiteNetLib.Utils; 5 | using System; 6 | 7 | namespace XProxy.Plugin.Core 8 | { 9 | public class ProxyConnection : UnityEngine.MonoBehaviour 10 | { 11 | private NetManager _manager; 12 | private EventBasedNetListener _listener; 13 | 14 | private bool _isConnecting; 15 | private DateTime _nextConnectiobRetry = DateTime.Now; 16 | private DateTime _nextStatusUpdate = DateTime.MinValue; 17 | void Awake() 18 | { 19 | _listener = new EventBasedNetListener(); 20 | 21 | _listener.PeerDisconnectedEvent += OnDisconnected; 22 | _listener.PeerConnectedEvent += OnConnected; 23 | 24 | _manager = new NetManager(_listener); 25 | _manager.Start(); 26 | } 27 | 28 | void SendIntialData() 29 | { 30 | SendStatus(); 31 | } 32 | 33 | void SendStatus() 34 | { 35 | if (!_manager.IsRunning || _manager.FirstPeer == null) 36 | return; 37 | 38 | NetDataWriter wr = new NetDataWriter(); 39 | wr.Put((byte)0); 40 | wr.Put((byte)MainClass.Status); 41 | 42 | _manager.FirstPeer.Send(wr, DeliveryMethod.ReliableOrdered); 43 | } 44 | 45 | void Update() 46 | { 47 | if (_manager == null) 48 | return; 49 | 50 | if (_manager.IsRunning) 51 | _manager.PollEvents(); 52 | 53 | if (!_isConnecting) 54 | { 55 | if (_nextConnectiobRetry < DateTime.Now) 56 | { 57 | NetDataWriter writer = new NetDataWriter(); 58 | 59 | // ClientType Proxy 60 | writer.Put((byte)2); 61 | writer.Put(MainClass.Singleton.Config.ConnectionKey); 62 | writer.Put(Server.Port); 63 | 64 | Logger.Info($"Connecting to proxy {MainClass.Singleton.Config.ProxyIP}:{MainClass.Singleton.Config.ProxyPort}..."); 65 | _manager.Connect(MainClass.Singleton.Config.ProxyIP, MainClass.Singleton.Config.ProxyPort, writer); 66 | _isConnecting = true; 67 | } 68 | } 69 | 70 | if (_manager.FirstPeer != null && DateTime.Now >= _nextStatusUpdate) 71 | { 72 | SendStatus(); 73 | _nextStatusUpdate = DateTime.Now.AddSeconds(10); 74 | } 75 | } 76 | 77 | 78 | void OnConnected(NetPeer peer) 79 | { 80 | Logger.Info($"Connected!"); 81 | SendIntialData(); 82 | } 83 | 84 | void OnDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) 85 | { 86 | _nextConnectiobRetry = DateTime.Now.AddSeconds(5); 87 | _isConnecting = false; 88 | Logger.Info($"Disconnected from proxy with reason {disconnectInfo.Reason}, reconnecting in 5 seconds..."); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /XProxy/Misc/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace XProxy.Misc 4 | { 5 | public class ConsoleLogger 6 | { 7 | public static bool AnsiDisabled { get; set; } 8 | 9 | public const char ESC = (char)27; 10 | 11 | public static string TimeString => DateTime.Now.TimeOfDay 12 | .ToString("hh\\:mm\\:ss") 13 | .ToString(); 14 | 15 | public static void Info(object message, string tag = null) => WriteLine($" (f=darkgray){TimeString}(f=white) [(f=cyan)INFO(f=white)] {(tag != null ? $"[(f=magenta){tag}(f=white)] " : string.Empty)}{message}"); 16 | public static void Error(object message, string tag = null) => WriteLine($" (f=darkgray){TimeString}(f=white) [(f=darkred)ERROR(f=white)] {(tag != null ? $"[(f=magenta){tag}(f=white)] " : string.Empty)}(f=red){message}"); 17 | public static void Warn(object message, string tag = null) => WriteLine($" (f=darkgray){TimeString}(f=white) [(f=darkyellow)WARN(f=white)] {(tag != null ? $"[(f=magenta){tag}(f=white)] " : string.Empty)}(f=yellow){message}"); 18 | 19 | static void WriteLine(object message) => Console.WriteLine(FormatAnsi(message)); 20 | 21 | public static string FormatAnsi(object message, bool forceRemove = false) 22 | { 23 | string text = message.ToString(); 24 | 25 | return Regex.Replace(text, @"\(f=(.*?)\)", ev => 26 | { 27 | if (AnsiDisabled || forceRemove) 28 | return string.Empty; 29 | 30 | string color = ev.Groups[1].Value.ToLower(); 31 | 32 | switch (color) 33 | { 34 | case "black": 35 | return $"{ESC}[30m"; 36 | 37 | case "darkred": 38 | return $"{ESC}[31m"; 39 | case "darkgreen": 40 | return $"{ESC}[32m"; 41 | case "darkyellow": 42 | return $"{ESC}[33m"; 43 | case "darkblue": 44 | return $"{ESC}[34m"; 45 | case "darkmagenta": 46 | return $"{ESC}[35m"; 47 | case "darkcyan": 48 | return $"{ESC}[36m"; 49 | case "darkgray": 50 | return $"{ESC}[90m"; 51 | 52 | case "gray": 53 | return $"{ESC}[37m"; 54 | case "red": 55 | return $"{ESC}[91m"; 56 | case "green": 57 | return $"{ESC}[92m"; 58 | case "yellow": 59 | return $"{ESC}[93m"; 60 | case "blue": 61 | return $"{ESC}[94m"; 62 | case "magenta": 63 | return $"{ESC}[95m"; 64 | case "cyan": 65 | return $"{ESC}[96m"; 66 | 67 | case "white": 68 | return $"{ESC}[97m"; 69 | 70 | default: 71 | return $"{ESC}[39m"; 72 | } 73 | }); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /XProxy.Core/Services/PublicKeyService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Newtonsoft.Json; 3 | using Org.BouncyCastle.Crypto; 4 | using System; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using XProxy.Cryptography; 9 | using XProxy.Models; 10 | 11 | namespace XProxy.Services 12 | { 13 | public class PublicKeyService : BackgroundService 14 | { 15 | public HttpClient Client; 16 | public static AsymmetricKeyParameter PublicKey; 17 | 18 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 19 | { 20 | Client = new HttpClient(); 21 | Client.DefaultRequestHeaders.Add("User-Agent", "SCP SL"); 22 | Client.DefaultRequestHeaders.Add("Game-Version", $"13.6.9"); 23 | 24 | string text = CentralServerKeyCache.ReadCache(); 25 | string text2 = string.Empty; 26 | string b = string.Empty; 27 | 28 | if (!string.IsNullOrEmpty(text)) 29 | { 30 | PublicKey = ECDSA.PublicKeyFromString(text); 31 | text2 = Sha.HashToString(Sha.Sha256(ECDSA.KeyToString(PublicKey))); 32 | Logger.Debug(ConfigService.Singleton.Messages.LoadedPublicKeyFromCache, "PublicKeyService"); 33 | } 34 | 35 | Logger.Debug(ConfigService.Singleton.Messages.DownloadPublicKeyFromCentrals, "PublicKeyService"); 36 | while (!stoppingToken.IsCancellationRequested) 37 | { 38 | string responseText = ""; 39 | try 40 | { 41 | HttpResponseMessage response = await Client.GetAsync(string.Format("{0}v4/publickey.php?major={1}", "https://api.scpslgame.com/", "13")); 42 | responseText = await response.Content.ReadAsStringAsync(); 43 | 44 | PublicKeyResponseModel publicKeyResponse = JsonConvert.DeserializeObject(responseText); 45 | if (!ECDSA.Verify(publicKeyResponse.Key, publicKeyResponse.Signature, CentralServerKeyCache.MasterKey)) 46 | { 47 | Logger.Error(ConfigService.Singleton.Messages.CantRefreshPublicKeyMessage, "PublicKeyService"); 48 | await Task.Delay(360000); 49 | continue; 50 | } 51 | PublicKey = ECDSA.PublicKeyFromString(publicKeyResponse.Key); 52 | string text3 = Sha.HashToString(Sha.Sha256(ECDSA.KeyToString(PublicKey))); 53 | if (text3 != b) 54 | { 55 | b = text3; 56 | Logger.Debug(ConfigService.Singleton.Messages.ObtainedPublicKeyMessage, "PublicKeyService"); 57 | if (text3 != text2) 58 | { 59 | CentralServerKeyCache.SaveCache(publicKeyResponse.Key, publicKeyResponse.Signature); 60 | } 61 | } 62 | } 63 | catch (Exception ex) 64 | { 65 | Logger.Error(ConfigService.Singleton.Messages.CantRefreshPublicKey2Message.Replace("%message%", ex.Message).Replace("%response%", responseText), "PublicKeyService"); 66 | } 67 | await Task.Delay(360000); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /XProxy.Core/Services/ConfigService.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using XProxy.Shared.Serialization; 3 | using XProxy.Models; 4 | using XProxy.Core; 5 | 6 | namespace XProxy.Services 7 | { 8 | public class ConfigService 9 | { 10 | public static string MainDirectory; 11 | 12 | public static ConfigService Singleton { get; internal set; } 13 | 14 | string _configPath { get; } = "config.yml"; 15 | string _languagesDir { get; set; } = "Languages"; 16 | 17 | string _defaultLanguagePath { get; } = "messages_en.yml"; 18 | 19 | public ConfigService() 20 | { 21 | if (!Directory.Exists(Path.Combine(MainDirectory, _languagesDir))) 22 | Directory.CreateDirectory(Path.Combine(MainDirectory, _languagesDir)); 23 | 24 | Load(true); 25 | 26 | Logger.Info(Messages.ProxyVersion.Replace("%version%", BuildInformation.VersionText).Replace("%gameVersion%", string.Join(", ", BuildInformation.SupportedGameVersions)), "XProxy"); 27 | } 28 | 29 | public ConfigModel Value { get; private set; } = new ConfigModel(); 30 | public MessagesModel Messages { get; private set; } = new MessagesModel(); 31 | 32 | public MessagesModel GetMessagesForLanguage(string language) 33 | { 34 | string path = Path.Combine(MainDirectory, this._languagesDir, $"messages_{language}.yml"); 35 | 36 | if (!File.Exists(path)) 37 | { 38 | // Get fallback. 39 | CreateIfMissing(); 40 | return YamlParser.Deserializer.Deserialize(File.ReadAllText(Path.Combine(MainDirectory, _defaultLanguagePath))); 41 | } 42 | 43 | return YamlParser.Deserializer.Deserialize(File.ReadAllText(path)); 44 | } 45 | 46 | void CreateIfMissing() 47 | { 48 | if (!File.Exists(Path.Combine(MainDirectory, _configPath))) 49 | File.WriteAllText(Path.Combine(MainDirectory, _configPath), YamlParser.Serializer.Serialize(new ConfigModel())); 50 | 51 | if (!File.Exists(Path.Combine(MainDirectory, _languagesDir, _defaultLanguagePath))) 52 | File.WriteAllText(Path.Combine(MainDirectory, _languagesDir, _defaultLanguagePath), YamlParser.Serializer.Serialize(new MessagesModel())); 53 | } 54 | 55 | public void Load(bool intial = false) 56 | { 57 | CreateIfMissing(); 58 | Value = YamlParser.Deserializer.Deserialize(File.ReadAllText(Path.Combine(MainDirectory, _configPath))); 59 | Logger.DebugMode = Value.Debug; 60 | Messages = GetMessagesForLanguage(Value.Langauge); 61 | 62 | if (!intial) 63 | Server.UpdateServers = true; 64 | 65 | Save(); 66 | Logger.Debug(Messages.ConfigLoadedMessage, "ConfigService"); 67 | } 68 | 69 | public void Save() 70 | { 71 | File.WriteAllText(Path.Combine(MainDirectory, _configPath), YamlParser.Serializer.Serialize(Value)); 72 | 73 | string path = Path.Combine(MainDirectory, this._languagesDir, $"messages_{Value.Langauge}.yml"); 74 | if (File.Exists(path)) 75 | File.WriteAllText(path, YamlParser.Serializer.Serialize(Messages)); 76 | 77 | Logger.Debug(Messages.ConfigSavedMessage, "ConfigService"); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /XProxy/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using System.Reflection; 5 | using XProxy.Misc; 6 | using XProxy.Models; 7 | using XProxy.Services; 8 | 9 | namespace XProxy 10 | { 11 | class Program 12 | { 13 | static async Task Main(string[] args) 14 | { 15 | HostApplicationBuilder applicationBuild = await BuildApplication(); 16 | 17 | SetupServices(applicationBuild); 18 | 19 | if (!SetupProxyServices(applicationBuild)) 20 | { 21 | ConsoleLogger.Info($"Press any key to exit.", "XProxy"); 22 | Console.ReadKey(); 23 | return; 24 | } 25 | 26 | await RunApplication(applicationBuild); 27 | } 28 | 29 | static async Task BuildApplication() 30 | { 31 | var builder = Host.CreateApplicationBuilder(); 32 | 33 | LauncherSettings.Load(); 34 | await UpdaterService.IntialRun(); 35 | 36 | builder.Logging.SetMinimumLevel(LogLevel.None); 37 | 38 | return builder; 39 | } 40 | 41 | static void SetupServices(HostApplicationBuilder builder) 42 | { 43 | IServiceCollection services = builder.Services; 44 | 45 | services.AddHostedService(); 46 | } 47 | 48 | static bool SetupProxyServices(HostApplicationBuilder builder) 49 | { 50 | IServiceCollection services = builder.Services; 51 | 52 | if (Directory.Exists(UpdaterService.DependenciesFolder)) 53 | { 54 | foreach (var dependency in Directory.GetFiles(UpdaterService.DependenciesFolder)) 55 | Assembly.LoadFrom(dependency); 56 | } 57 | 58 | if (!File.Exists(UpdaterService.ProxyFile)) 59 | { 60 | ConsoleLogger.Error($"XProxy.Core.dll file not found, aborting startup!", "XProxy"); 61 | return false; 62 | } 63 | 64 | AppDomain.CurrentDomain.AssemblyResolve += (o, e) => 65 | { 66 | AssemblyName aName = new AssemblyName(e.Name); 67 | 68 | switch (aName.Name) 69 | { 70 | case "XProxy.Core": 71 | return Assembly.LoadFrom(UpdaterService.ProxyFile); 72 | default: 73 | Console.WriteLine($"Failed to resolve assembly for " + aName.Name); 74 | return null; 75 | } 76 | }; 77 | 78 | 79 | try 80 | { 81 | InitializeProxy(services); 82 | } 83 | catch (Exception ex) 84 | { 85 | ConsoleLogger.Error($"Failed to setup services for XProxy, aborting startup!\n{ex}", "XProxy"); 86 | return false; 87 | } 88 | 89 | return true; 90 | } 91 | 92 | static void InitializeProxy(IServiceCollection services) 93 | { 94 | Logger.AnsiDisabled = LauncherSettings.Value.DisableAnsiColors; 95 | Initializer.SetupServices(services); 96 | } 97 | 98 | static async Task RunApplication(HostApplicationBuilder app) 99 | { 100 | IHost host = app.Build(); 101 | 102 | await host.RunAsync(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /XProxy.BuildListing/Program.cs: -------------------------------------------------------------------------------- 1 | using McMaster.Extensions.CommandLineUtils; 2 | using Microsoft.Extensions.Hosting; 3 | using Newtonsoft.Json; 4 | using Octokit; 5 | using System.ComponentModel.DataAnnotations; 6 | using XProxy.Models; 7 | 8 | await Host.CreateDefaultBuilder() 9 | .RunCommandLineApplicationAsync(args); 10 | 11 | [Command(Description = "Runs listing builder.")] 12 | public class AppCommand 13 | { 14 | public const float KillDamage = float.PositiveInfinity; 15 | 16 | private GitHubClient _gitHubClient; 17 | public GitHubClient Client 18 | { 19 | get 20 | { 21 | if (_gitHubClient == null) 22 | { 23 | _gitHubClient = new GitHubClient(new ProductHeaderValue("XProxy.Listing")); 24 | _gitHubClient.Credentials = new Credentials(Token); 25 | } 26 | 27 | return _gitHubClient; 28 | } 29 | } 30 | 31 | private HttpClient _http; 32 | public HttpClient Http 33 | { 34 | get 35 | { 36 | if (_http == null) 37 | { 38 | _http = new HttpClient(); 39 | _http.DefaultRequestHeaders.UserAgent.ParseAdd("XProxy Listing 1.0.0"); 40 | } 41 | 42 | return _http; 43 | } 44 | } 45 | 46 | public string Repository => Environment.GetEnvironmentVariable("GITHUB_REPOSITORY"); 47 | public string RepositoryOwner => Environment.GetEnvironmentVariable("GITHUB_REPOSITORY_OWNER"); 48 | 49 | [Required] 50 | [Option(Description = "Build.")] 51 | public string Token { get; set; } = null; 52 | 53 | public async Task OnExecute(IConsole console) 54 | { 55 | try 56 | { 57 | string[] sp = Repository.Split("/"); 58 | 59 | string repositoryName = sp[1]; 60 | 61 | var releases = await Client.Repository.Release.GetAll(RepositoryOwner, repositoryName); 62 | 63 | if (releases.Count == 0) 64 | { 65 | Console.WriteLine("0 releases found!"); 66 | return 1; 67 | } 68 | 69 | BuildsListing listing = new BuildsListing(); 70 | 71 | foreach (var release in releases) 72 | { 73 | foreach (var asset in release.Assets) 74 | { 75 | string fileName = Path.GetFileName(asset.BrowserDownloadUrl); 76 | 77 | switch (fileName.ToUpper()) 78 | { 79 | case "RELEASEINFO.JSON": 80 | 81 | var result = await Http.GetAsync(asset.BrowserDownloadUrl); 82 | 83 | if (result.IsSuccessStatusCode) 84 | { 85 | var build = JsonConvert.DeserializeObject(await result.Content.ReadAsStringAsync()); 86 | listing.Builds.Add(build.Version, build); 87 | } 88 | break; 89 | } 90 | } 91 | 92 | Console.WriteLine($"Add Version {release.TagName}"); 93 | } 94 | 95 | File.WriteAllText("./Website/builds.json", JsonConvert.SerializeObject(listing, Formatting.Indented)); 96 | Console.WriteLine("Builds file uploaded!"); 97 | return 0; 98 | } 99 | catch (Exception ex) 100 | { 101 | console.WriteLine(ex); 102 | return 1; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace XProxy 6 | { 7 | public class Logger 8 | { 9 | public static ConcurrentQueue NewLogEntry = new ConcurrentQueue(); 10 | 11 | public const char ESC = (char)27; 12 | 13 | public static bool AnsiDisabled { get; set; } 14 | public static bool DebugMode { get; set; } 15 | 16 | public static DateTime SessionTime = DateTime.Now; 17 | 18 | static string TimeString => DateTime.Now.TimeOfDay 19 | .ToString("hh\\:mm\\:ss") 20 | .ToString(); 21 | 22 | public static void Info(object message, string tag = null) => WriteLine($" (f=darkgray){TimeString}(f=white) [(f=cyan)INFO(f=white)] {(tag != null ? $"[(f=magenta){tag}(f=white)] " : string.Empty)}{message}"); 23 | public static void Error(object message, string tag = null) => WriteLine($" (f=darkgray){TimeString}(f=white) [(f=darkred)ERROR(f=white)] {(tag != null ? $"[(f=magenta){tag}(f=white)] " : string.Empty)}(f=red){message}"); 24 | public static void Warn(object message, string tag = null) => WriteLine($" (f=darkgray){TimeString}(f=white) [(f=darkyellow)WARN(f=white)] {(tag != null ? $"[(f=magenta){tag}(f=white)] " : string.Empty)}(f=yellow){message}"); 25 | public static void Debug(object message, string tag = null) 26 | { 27 | if (DebugMode) 28 | WriteLine($" (f=darkgray){TimeString}(f=white) [(f=yellow)DEBUG(f=white)] {(tag != null ? $"[(f=magenta){tag}(f=white)] " : string.Empty)}(f=yellow){message}"); 29 | } 30 | 31 | static void WriteLine(object message) 32 | { 33 | NewLogEntry.Enqueue(message.ToString()); 34 | } 35 | 36 | public static string FormatAnsi(object message, bool forceRemove = false) 37 | { 38 | string text = message.ToString(); 39 | 40 | return Regex.Replace(text, @"\(f=(.*?)\)", ev => 41 | { 42 | if (AnsiDisabled || forceRemove) 43 | return string.Empty; 44 | 45 | string color = ev.Groups[1].Value.ToLower(); 46 | 47 | switch (color) 48 | { 49 | case "black": 50 | return $"{ESC}[30m"; 51 | 52 | case "darkred": 53 | return $"{ESC}[31m"; 54 | case "darkgreen": 55 | return $"{ESC}[32m"; 56 | case "darkyellow": 57 | return $"{ESC}[33m"; 58 | case "darkblue": 59 | return $"{ESC}[34m"; 60 | case "darkmagenta": 61 | return $"{ESC}[35m"; 62 | case "darkcyan": 63 | return $"{ESC}[36m"; 64 | case "darkgray": 65 | return $"{ESC}[90m"; 66 | 67 | case "gray": 68 | return $"{ESC}[37m"; 69 | case "red": 70 | return $"{ESC}[91m"; 71 | case "green": 72 | return $"{ESC}[92m"; 73 | case "yellow": 74 | return $"{ESC}[93m"; 75 | case "blue": 76 | return $"{ESC}[94m"; 77 | case "magenta": 78 | return $"{ESC}[95m"; 79 | case "cyan": 80 | return $"{ESC}[96m"; 81 | 82 | case "white": 83 | return $"{ESC}[97m"; 84 | 85 | default: 86 | return $"{ESC}[39m"; 87 | } 88 | }); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /XProxy.Core/Models/ConfigModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel; 3 | using XProxy.Core.Models; 4 | using XProxy.Enums; 5 | using YamlDotNet.Serialization; 6 | 7 | namespace XProxy.Models 8 | { 9 | public class ConfigModel 10 | { 11 | [Description("Enables debug logs.")] 12 | public bool Debug { get; set; } 13 | 14 | [Description("If set to true updater will not try to update XProxy on startup.")] 15 | public bool DisableUpdater { get; set; } = false; 16 | 17 | [Description("For which version of game XProxy will be downloaded")] 18 | public string GameVersion { get; set; } = "latest"; 19 | 20 | [Description("Language of messages.")] 21 | public string Langauge { get; set; } = "en"; 22 | 23 | [Description("Email used for listing your server on SCP SL serverlist.")] 24 | public string Email { get; set; } = "example@gmail.com"; 25 | 26 | [Description("All listeners which will host own proxy instance.")] 27 | public Dictionary Listeners { get; set; } = new Dictionary() 28 | { 29 | { "main", new ListenerServer() } 30 | }; 31 | 32 | [Description("Northwood staff ignores maximum amount of players which can connect to proxy.")] 33 | public bool NorthwoodStaffIgnoresSlots { get; set; } = false; 34 | 35 | [Description("Available servers.")] 36 | public Dictionary Servers { get; set; } = new Dictionary() 37 | { 38 | { "lobby", new ServerModel() 39 | { 40 | Name = "Lobby", 41 | Ip = "127.0.0.1", 42 | PublicIp = "127.0.0.1", 43 | Port = 7777, 44 | MaxPlayers = 50, 45 | ConnectionType = ConnectionType.Simulated, 46 | Simulation = "lobby", 47 | SendIpAddressInPreAuth = false, 48 | } 49 | }, 50 | { "vanilla", new ServerModel() 51 | { 52 | Name = "Vanilla", 53 | Ip = "127.0.0.1", 54 | PublicIp = "127.0.0.1", 55 | Port = 7778, 56 | } 57 | } 58 | }; 59 | 60 | [Description("User permissions")] 61 | public Dictionary Users { get; set; } = new Dictionary() 62 | { 63 | { "admin@admin", new UserModel() 64 | { 65 | IgnoreMaintenance = true, 66 | } 67 | } 68 | }; 69 | 70 | [Description("If maintenance mode is enabled.")] 71 | public bool MaintenanceMode { get; set; } 72 | [Description("Name of server visbile on serverlist when maintenance mode is enabled.")] 73 | [YamlMember(ScalarStyle = YamlDotNet.Core.ScalarStyle.Literal)] 74 | public string MaintenanceServerName { get; set; } = 75 | @"Example server name - Maintenance 76 | Proxy Server"; 77 | 78 | [Description("Player will be added to queue for first server from priorities if its full.")] 79 | public bool AutoJoinQueueInLobby { get; set; } = false; 80 | [Description("For how long queue slot will be valid for player upon joining target server. ( started connecting to target server >-( TIME IN SECONDS )-> connected to target server ) ")] 81 | public int QueueTicketLifetime { get; set; } = 15; 82 | 83 | public bool TryGetServer(string serverName, out ServerModel serverModel) 84 | { 85 | if (Servers.TryGetValue(serverName, out serverModel)) 86 | return true; 87 | 88 | serverModel = null; 89 | return false; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /XProxy.Core/Models/ListenerServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using YamlDotNet.Serialization; 7 | 8 | namespace XProxy.Models 9 | { 10 | public class ListenerServer 11 | { 12 | private Version _version; 13 | private HttpClient _httpClient; 14 | 15 | #region YAML Settings 16 | 17 | [Description("This IP is used for UDP Server to listen on.")] 18 | public string ListenIp { get; set; } = "0.0.0.0"; 19 | 20 | [Description("This PORT is used for listing your server on SCPSL Server List and for UDP Server to listen on.")] 21 | public ushort Port { get; set; } = 7777; 22 | 23 | [Description("Maximum amount of players which can connect to server.")] 24 | public int MaxPlayers { get; set; } = 50; 25 | 26 | [Description("Version of game for which listener will run, this version is also used for listing your server on SCP Server List.")] 27 | public string Version { get; set; } = "14.0.2"; 28 | 29 | [Description("Priority servers used for first connection and fallback servers.")] 30 | public List Priorities { get; set; } = new List() { "lobby" }; 31 | 32 | [Description("Settings related to SCP Server List.")] 33 | public ScpServerList ServerList { get; set; } = new ScpServerList(); 34 | #endregion 35 | 36 | /// 37 | /// Gets http client for this listener. 38 | /// 39 | [YamlIgnore] 40 | public HttpClient Http 41 | { 42 | get 43 | { 44 | if (_httpClient == null) 45 | { 46 | _httpClient = new HttpClient(); 47 | _httpClient.DefaultRequestHeaders.Add("User-Agent", "SCP SL"); 48 | _httpClient.DefaultRequestHeaders.Add("Game-Version", Version); 49 | } 50 | 51 | return _httpClient; 52 | } 53 | } 54 | 55 | /// 56 | /// Gets final public ip of this listener. 57 | /// 58 | public string PublicIp; 59 | 60 | /// 61 | /// If data on serverlist should be updated. 62 | /// 63 | public bool ServerListUpdate; 64 | 65 | /// 66 | /// Gets cycle number of server listing. 67 | /// 68 | public int ServerListCycle; 69 | 70 | [YamlIgnore] 71 | public System.Version GameVersionParsed 72 | { 73 | get 74 | { 75 | if (_version == null) 76 | { 77 | string text = Version.Contains("-") ? Version.Split('-')[0] : Version; 78 | 79 | System.Version.TryParse(text, out _version); 80 | } 81 | 82 | return _version; 83 | } 84 | } 85 | 86 | /// 87 | /// Initializes this listener. 88 | /// 89 | /// 90 | public async Task Initialize() 91 | { 92 | if (ServerList.AddressIp != "auto") 93 | PublicIp = ServerList.AddressIp; 94 | else 95 | PublicIp = await GetPublicIp(); 96 | } 97 | 98 | async Task GetPublicIp() 99 | { 100 | try 101 | { 102 | using (var response = await Http.GetAsync("https://api.scpslgame.com/ip.php")) 103 | { 104 | string str = await response.Content.ReadAsStringAsync(); 105 | 106 | str = (str.EndsWith(".") ? str.Remove(str.Length - 1) : str); 107 | 108 | return str; 109 | } 110 | } 111 | catch (Exception ex) 112 | { 113 | Logger.Error(ex, "ListService"); 114 | return null; 115 | } 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/Extensions.cs: -------------------------------------------------------------------------------- 1 | using LiteNetLib; 2 | using LiteNetLib.Utils; 3 | using Mirror; 4 | using System; 5 | using XProxy; 6 | using XProxy.Core.Events; 7 | using static XProxy.Core.Events.EventManager; 8 | 9 | public static class Extensions 10 | { 11 | public static byte[] ReadByteArray(this NetworkReader reader) 12 | { 13 | int size = reader.ReadInt(); 14 | byte[] data = new byte[size]; 15 | 16 | for (int x = 0; x < size; x++) 17 | { 18 | data[x] = reader.ReadByte(); 19 | } 20 | return data; 21 | } 22 | 23 | public static void WriteByteArray(this NetworkWriter writer, byte[] array) 24 | { 25 | writer.WriteInt(array.Length); 26 | for (int x = 0; x < array.Length; x++) 27 | { 28 | writer.WriteByte(array[x]); 29 | } 30 | } 31 | 32 | public static void WriteArraySegment(this NetworkWriter writer, ArraySegment array) 33 | { 34 | if (array == null) 35 | { 36 | writer.WriteUInt(0U); 37 | return; 38 | } 39 | writer.WriteUInt(checked((uint)array.Count) + 1U); 40 | writer.WriteBytes(array.Array, array.Offset, array.Count); 41 | } 42 | 43 | public static void Disconnect(this ConnectionRequest request, string message) 44 | { 45 | NetDataWriter writer = new NetDataWriter(); 46 | writer.Put((byte)RejectionReason.Custom); 47 | writer.Put(message); 48 | request.Reject(writer); 49 | } 50 | 51 | public static void DisconnectBanned(this ConnectionRequest request, string reason, long expiration) 52 | { 53 | NetDataWriter writer = new NetDataWriter(); 54 | writer.Put((byte)RejectionReason.Banned); 55 | writer.Put(expiration); 56 | writer.Put(reason); 57 | request.Reject(writer); 58 | } 59 | 60 | public static void DisconnectBanned(this ConnectionRequest request, string reason, DateTime date) => request.DisconnectBanned(reason, date.Ticks); 61 | 62 | public static void DisconnectServerFull(this ConnectionRequest request) 63 | { 64 | NetDataWriter writer = new NetDataWriter(); 65 | writer.Put((byte)RejectionReason.ServerFull); 66 | request.Reject(writer); 67 | } 68 | 69 | public static void DisconnectWrongVersion(this ConnectionRequest request) 70 | { 71 | NetDataWriter writer = new NetDataWriter(); 72 | writer.Put((byte)RejectionReason.VersionMismatch); 73 | request.Reject(writer); 74 | } 75 | 76 | public static string ToReadableString(this TimeSpan span) 77 | { 78 | string formatted = string.Format("{0}{1}{2}{3}", 79 | span.Duration().Days > 0 ? string.Format("{0:0} day{1}, ", span.Days, span.Days == 1 ? string.Empty : "s") : string.Empty, 80 | span.Duration().Hours > 0 ? string.Format("{0:0} hour{1}, ", span.Hours, span.Hours == 1 ? string.Empty : "s") : string.Empty, 81 | span.Duration().Minutes > 0 ? string.Format("{0:0} minute{1}, ", span.Minutes, span.Minutes == 1 ? string.Empty : "s") : string.Empty, 82 | span.Duration().Seconds > 0 ? string.Format("{0:0} second{1}", span.Seconds, span.Seconds == 1 ? string.Empty : "s") : string.Empty); 83 | 84 | if (formatted.EndsWith(", ")) formatted = formatted.Substring(0, formatted.Length - 2); 85 | 86 | if (string.IsNullOrEmpty(formatted)) formatted = "0 seconds"; 87 | 88 | return formatted; 89 | } 90 | 91 | public static void InvokeWithExceptionHandler(this CustomEventHandler ev, TEvent arguments) where TEvent: BaseEvent 92 | { 93 | foreach(var invoker in ev.GetInvocationList()) 94 | { 95 | if (invoker is not CustomEventHandler customInvoker) continue; 96 | 97 | try 98 | { 99 | customInvoker.Invoke(arguments); 100 | } 101 | catch (Exception ex) 102 | { 103 | Logger.Error($"Exception while invoking event (f=green){ev.GetType().Name}(f=red) {ex}", "EventManager"); 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /XProxy.Core/Services/CommandsService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Reflection; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using XProxy.Attributes; 10 | using static Org.BouncyCastle.Math.EC.ECCurve; 11 | 12 | namespace XProxy.Services 13 | { 14 | public class CommandsService : BackgroundService 15 | { 16 | public delegate void CommandDelegate(CommandsService service, string[] args); 17 | 18 | public static Dictionary Commands { get; private set; } = new Dictionary(); 19 | 20 | public static void RegisterConsoleCommandsInAssembly(Assembly assembly) 21 | { 22 | foreach (var type in assembly.GetTypes()) 23 | { 24 | foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) 25 | { 26 | var ev = method.GetCustomAttribute(); 27 | 28 | if (ev == null) continue; 29 | 30 | if (Commands.ContainsKey(ev.Name.ToLower())) 31 | { 32 | Logger.Warn(ConfigService.Singleton.Messages.CommandAlreadyRegisteredMessage.Replace("%name%", ev.Name), "CommandsService"); 33 | continue; 34 | } 35 | 36 | Delegate del = Delegate.CreateDelegate(typeof(CommandDelegate), method); 37 | Commands.Add(ev.Name.ToLower(), del); 38 | Logger.Info(ConfigService.Singleton.Messages.CommandRegisteredMessage.Replace("%name%", ev.Name), "CommandsService"); 39 | } 40 | } 41 | } 42 | 43 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 44 | { 45 | while (!stoppingToken.IsCancellationRequested) 46 | { 47 | string cmd = Console.ReadLine(); 48 | 49 | if (string.IsNullOrEmpty(cmd)) continue; 50 | 51 | string[] args = cmd.Split(' '); 52 | 53 | if (Commands.TryGetValue(args[0].ToLower(), out Delegate del)) 54 | { 55 | if (del is CommandDelegate d2) 56 | { 57 | try 58 | { 59 | d2?.Invoke(this, args.Skip(1).ToArray()); 60 | } 61 | catch (Exception ex) 62 | { 63 | Logger.Error($"Failed to execute command {cmd}\n" + ex); 64 | } 65 | } 66 | } 67 | else 68 | { 69 | Logger.Info(ConfigService.Singleton.Messages.CommandNotExistsMessage.Replace("%name%", args[0]), "CommandsService"); 70 | } 71 | 72 | await Task.Delay(15); 73 | } 74 | } 75 | 76 | public async Task RunCentralServerCommand(Listener server, string cmd, string args) 77 | { 78 | cmd = cmd.ToLower(); 79 | 80 | Dictionary data = new Dictionary() 81 | { 82 | { "ip", server.Settings.PublicIp }, 83 | { "port", $"{server.Settings.Port}" }, 84 | { "cmd", ListService.Base64Encode(cmd) }, 85 | { "args", ListService.Base64Encode(args) }, 86 | }; 87 | 88 | if (!string.IsNullOrEmpty(ListService.Password)) 89 | data.Add("passcode", ListService.Password); 90 | 91 | using (var response = await server.Settings.Http.PostAsync($"https://api.scpslgame.com/centralcommands/{cmd}.php", new FormUrlEncodedContent(data))) 92 | { 93 | string text = await response.Content.ReadAsStringAsync(); 94 | 95 | Logger.Info(ConfigService.Singleton.Messages.CentralCommandMessage.Replace("%command%", cmd).Replace("%message%", text), $"ListService"); 96 | } 97 | } 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /XProxy.Core/Cryptography/Sha.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | using NorthwoodLib.Pools; 6 | 7 | namespace XProxy.Cryptography 8 | { 9 | public static class Sha 10 | { 11 | public static byte[] Sha1(byte[] message) 12 | { 13 | // Disposed un-disposed SHA1 @ Dankrushen 14 | using (var sha1 = SHA1.Create()) 15 | { 16 | return sha1.ComputeHash(message); 17 | } 18 | } 19 | 20 | public static byte[] Sha1(byte[] message, int offset, int length) 21 | { 22 | // Disposed un-disposed SHA1 @ Dankrushen 23 | using (var sha1 = SHA1.Create()) 24 | { 25 | return sha1.ComputeHash(message, offset, length); 26 | } 27 | } 28 | 29 | public static byte[] Sha1(string message) 30 | { 31 | byte[] buffer = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(message.Length)); 32 | int length = Utf8.GetBytes(message, buffer); 33 | byte[] result = Sha1(buffer, 0, length); 34 | ArrayPool.Shared.Return(buffer); 35 | return result; 36 | } 37 | 38 | public static byte[] Sha256(byte[] message) 39 | { 40 | // Disposed un-disposed SHA256 @ Dankrushen 41 | using (var sha256 = SHA256.Create()) 42 | { 43 | return sha256.ComputeHash(message); 44 | } 45 | } 46 | 47 | public static byte[] Sha256(byte[] message, int offset, int length) 48 | { 49 | // Disposed un-disposed SHA256 @ Dankrushen 50 | using (var sha256 = SHA256.Create()) 51 | { 52 | return sha256.ComputeHash(message, offset, length); 53 | } 54 | } 55 | 56 | public static byte[] Sha256(string message) 57 | { 58 | byte[] buffer = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(message.Length)); 59 | int length = Utf8.GetBytes(message, buffer); 60 | byte[] result = Sha256(buffer, 0, length); 61 | ArrayPool.Shared.Return(buffer); 62 | return result; 63 | } 64 | 65 | #if !HEADLESS 66 | public static byte[] Sha256File(string path) 67 | { 68 | using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) 69 | using (var sha256 = SHA256.Create()) 70 | return sha256.ComputeHash(fs); 71 | } 72 | #endif 73 | 74 | public static byte[] Sha256Hmac(byte[] key, byte[] message) 75 | { 76 | // Disposed un-disposed HMACSHA256 @ Dankrushen 77 | using (var hash = new HMACSHA256(key)) 78 | { 79 | return hash.ComputeHash(message); 80 | } 81 | } 82 | 83 | public static byte[] Sha512(string message) 84 | { 85 | byte[] buffer = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(message.Length)); 86 | int length = Utf8.GetBytes(message, buffer); 87 | byte[] result = Sha512(buffer, 0, length); 88 | ArrayPool.Shared.Return(buffer); 89 | return result; 90 | } 91 | 92 | public static byte[] Sha512(byte[] message) 93 | { 94 | // Disposed un-disposed SHA512 @ Dankrushen 95 | using (var sha512 = SHA512.Create()) 96 | { 97 | return sha512.ComputeHash(message); 98 | } 99 | } 100 | 101 | public static byte[] Sha512(byte[] message, int offset, int length) 102 | { 103 | // Disposed un-disposed SHA512 @ Dankrushen 104 | using (var sha512 = SHA512.Create()) 105 | { 106 | return sha512.ComputeHash(message, offset, length); 107 | } 108 | } 109 | 110 | public static byte[] Sha512Hmac(byte[] key, byte[] message) 111 | { 112 | // Disposed un-disposed HMACSHA512 @ Dankrushen 113 | using (var hash = new HMACSHA512(key)) 114 | { 115 | return hash.ComputeHash(message); 116 | } 117 | } 118 | 119 | public static byte[] Sha512Hmac(byte[] key, int offset, int length, byte[] message) 120 | { 121 | // Disposed un-disposed HMACSHA512 @ Dankrushen 122 | using (var hash = new HMACSHA512(key)) 123 | { 124 | return hash.ComputeHash(message, offset, length); 125 | } 126 | } 127 | 128 | public static string HashToString(byte[] hash) 129 | { 130 | var result = StringBuilderPool.Shared.Rent(); 131 | foreach (var t in hash) 132 | result.Append(t.ToString("X2")); 133 | string text = result.ToString(); 134 | StringBuilderPool.Shared.Return(result); 135 | 136 | return text; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Killers0992/XProxy/total?label=Downloads&labelColor=2e343e&color=00FFFF&style=for-the-badge) 2 | [![Discord](https://img.shields.io/discord/1216429195232673964?label=Discord&labelColor=2e343e&color=00FFFF&style=for-the-badge)](https://discord.gg/czQCAsDMHa) 3 | # XProxy 4 | Proxy for SCP: Secret Laboratory allowing you to link multiple servers into one! 5 | 6 | Features 7 | - Virtual lobby - Allowing you to select specific server which you want to connect to. 8 | - Queue system - If server is full you will be added to queue and when system detects if theres free slot and then you will be connected. 9 | - Fallback system - Server shutdowns or timeouts without any reason you will be connected to available server. 10 | 11 | **Pterodactyl Egg** 12 | [proxy.json](https://github.com/Killers0992/XProxy/blob/master/Storage/egg-s-c-p--s-l-proxy.json) 13 | 14 | # Setup 15 | 1. Depending if you use linux or windows download proper installer 16 | - [Windows x64](https://github.com/Killers0992/XProxy/releases/latest/download/XProxy.exe) 17 | - [Linux x64](https://github.com/Killers0992/XProxy/releases/latest/download/XProxy) 18 | 19 | 2. Make sure you have Dotnet 8.0 Runtime installed 20 | - [Windows](https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win10-x64&apphost_version=8.0.0) 21 | - [Linux](https://learn.microsoft.com/dotnet/core/install/linux?WT.mc_id=dotnet-35129-website) 22 | 23 | 3. Run proxy by using **XProxy.exe** on windows or **XProxy** on linux. 24 | 4. Configure **config.yml** inside **Data** folder, if you have verkey you need to create **verkey.txt** and put verkey here. 25 | 5. Every server under proxy needs to be hidden on serverlist **CENTRAL COMMAND** !private and **config_gameplay.txt** needs to have 26 | ```yaml 27 | enable_ip_ratelimit: false 28 | 29 | enable_proxy_ip_passthrough: true 30 | trusted_proxies_ip_addresses: 31 | - 32 | ``` 33 | - Replace ```` with public ip if your proxy is not running on same machine or with local ip, if you dont know which ip to set just before adding ip just connect to server via proxy and check console. 34 | 35 | # Placeholders 36 | These placeholders can be used in lobby hint or server name. 37 | 38 | | Placeholder | Desc | 39 | | ------------- | ------------- | 40 | | ``%playersInQueue_%`` | Shows amount of players in queue to specific server. | 41 | | ``%onlinePlayers_%`` | Shows amount of online players on specific server. | 42 | | ``%maxPlayers_%`` | Shows amount of maximum players on specific | 43 | | ``%proxyOnlinePlayers%`` | Shows total amount of connected players to proxy. | 44 | | ``%proxyMaxPlayers_%`` | Shows maximum amount of player which can connect to specific listener. | 45 | 46 | # FAQ 47 | - If you see logs like that ![image](https://github.com/Killers0992/XProxy/assets/38152961/0e7c4374-021a-4618-bb2e-b268286fd3cf) this means your console is not supporting ANSI colors ! 48 | - 49 | Inside ``config.yml`` change ``AnsiColors`` to ``false`` ! 50 | 51 | # Console Commands 52 | | Command | Arguments | Description | 53 | | ------------- | ------------- | ------------- | 54 | | servers | | Shows all servers. | 55 | | players | | Shows players playing on servers. | 56 | | listeners | | Shows all listeners. | 57 | | send | ``all/serverName/id@steam`` ``serverName`` | Sends all players, entire population of a server or target to specific server. | 58 | | maintenance toggle | | Toggles maintenance | 59 | | maintenance servername | ``name`` | Changes server name set when maintenance is enabled. | 60 | | reload | | Reloads configs. | 61 | | sendhint | ``message`` | Sends hint to all players. | 62 | | broadcast | ``time`` ``message`` | Sends broadcast to all players. | 63 | | runcentralcmd | ``listenername`` ``command`` | Runs central command on specific listener. | 64 | 65 | # Lobby Commands 66 | | Command | Arguments | Description | 67 | | ------------- | ------------- | ------------- | 68 | | .connect | ``serverName`` | Connects you to specific server. | 69 | | .servers | | Shows all servers. | 70 | 71 | # Queue Commands 72 | | Command | Arguments | Description | 73 | | ------------- | ------------- | ------------- | 74 | | .hub / .lobby | | Sends you back to lobby. | 75 | -------------------------------------------------------------------------------- /XProxy.Core/Misc/CentralServerKeyCache.cs: -------------------------------------------------------------------------------- 1 | using Org.BouncyCastle.Crypto; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using XProxy.Cryptography; 10 | 11 | namespace XProxy 12 | { 13 | public static class CentralServerKeyCache 14 | { 15 | public static string ReadCache() 16 | { 17 | string result; 18 | try 19 | { 20 | if (!File.Exists("./centralcache.txt")) 21 | { 22 | Logger.Info($"Central server public key not found in cache.", $"CentralServerKeyCache"); 23 | result = null; 24 | } 25 | else if (!File.Exists("./centralkeysignature.txt")) 26 | { 27 | Logger.Info($"Central server public key signature not found in cache.", $"CentralServerKeyCache"); 28 | result = null; 29 | } 30 | else 31 | { 32 | string[] source = File.ReadAllLines("./centralcache.txt"); 33 | string[] array = File.ReadAllLines("./centralkeysignature.txt"); 34 | if (array.Length == 0) 35 | { 36 | Logger.Error($"Can't load central server public key from cache - empty signature.", $"CentralServerKeyCache"); 37 | result = null; 38 | } 39 | else 40 | { 41 | string text = source.Aggregate("", (string current, string line) => current + line + "\r\n").Trim(); 42 | try 43 | { 44 | if (ECDSA.Verify(text, array[0], CentralServerKeyCache.MasterKey)) 45 | { 46 | result = text; 47 | } 48 | else 49 | { 50 | Logger.Error($"Invalid signature of Central Server Key in cache!", $"CentralServerKeyCache"); 51 | result = null; 52 | } 53 | } 54 | catch (Exception ex) 55 | { 56 | Logger.Error($"Can't load central server public key from cache - " + ex.Message, $"CentralServerKeyCache"); 57 | result = null; 58 | } 59 | } 60 | } 61 | } 62 | catch (Exception ex2) 63 | { 64 | 65 | Logger.Error($"Can't read public key cache - " + ex2.Message, $"CentralServerKeyCache"); 66 | result = null; 67 | } 68 | return result; 69 | } 70 | 71 | public static void SaveCache(string key, string signature) 72 | { 73 | try 74 | { 75 | if (!ECDSA.Verify(key, signature, CentralServerKeyCache.MasterKey)) 76 | { 77 | Logger.Error($"Invalid signature of Central Server Key!", $"CentralServerKeyCache"); 78 | } 79 | else 80 | { 81 | if (File.Exists("./centralcache.txt")) 82 | { 83 | if (key == CentralServerKeyCache.ReadCache()) 84 | { 85 | Logger.Info($"Key cache is up to date.", $"CentralServerKeyCache"); 86 | return; 87 | } 88 | File.Delete("./centralcache.txt"); 89 | } 90 | 91 | Logger.Info($"Updating key cache...", $"CentralServerKeyCache"); 92 | File.WriteAllText($"./centralcache.txt", key, Encoding.UTF8); 93 | File.WriteAllText($"./centralkeysignature.txt", signature, Encoding.UTF8); 94 | Logger.Info($"Key cache updated!", $"CentralServerKeyCache"); 95 | } 96 | } 97 | catch (Exception ex) 98 | { 99 | Logger.Error("Can't write public key cache - " + ex.Message, $"CentralServerKeyCache"); 100 | } 101 | } 102 | 103 | internal static readonly AsymmetricKeyParameter MasterKey = Cryptography.ECDSA.PublicKeyFromString("-----BEGIN PUBLIC KEY-----\r\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAbL0YvrhVB2meqCq5XzjAJD8Ii0hb\r\nBHdIQ587N583cP8twjDhcITjZhBHJPJDuA85XdpgG04HwT0SD3WcAvoQXBUAUsG1\r\nLS9TR4urHwfgfroq4tH2HAQE6ZxFZeIFSglLO8nxySim4yKBj96HLG624lzKvzoD\r\nId+GOwjcd3XskOq9Dwc=\r\n-----END PUBLIC KEY-----"); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /XProxy.BuildRelease/Program.cs: -------------------------------------------------------------------------------- 1 | using McMaster.Extensions.CommandLineUtils; 2 | using Microsoft.Extensions.Hosting; 3 | using Newtonsoft.Json; 4 | using System.IO.Compression; 5 | using System.Reflection; 6 | using XProxy.Models; 7 | 8 | await Host.CreateDefaultBuilder() 9 | .RunCommandLineApplicationAsync(args); 10 | 11 | [Command(Description = "Runs release builder.")] 12 | public class AppCommand 13 | { 14 | private HttpClient _http; 15 | public HttpClient Http 16 | { 17 | get 18 | { 19 | if (_http == null) 20 | { 21 | _http = new HttpClient(); 22 | _http.DefaultRequestHeaders.UserAgent.ParseAdd("XProxy Release 1.0.0"); 23 | } 24 | 25 | return _http; 26 | } 27 | } 28 | 29 | public string SlReferences => Environment.GetEnvironmentVariable("SL_REFERENCES"); 30 | 31 | public string Workspace => Environment.GetEnvironmentVariable("GITHUB_WORKSPACE"); 32 | public string MainPath => Path.Combine(Workspace, "main"); 33 | 34 | public string XProxyCoreProject => Path.Combine(MainPath, "XProxy.Core"); 35 | 36 | public async Task OnExecute(IConsole console) 37 | { 38 | try 39 | { 40 | string releaseFolder = Path.Combine(XProxyCoreProject, "bin", "Release", "net8.0", "win-x64"); 41 | 42 | string coreFile = Path.Combine(releaseFolder, "XProxy.Core.dll"); 43 | 44 | if (!File.Exists(coreFile)) 45 | { 46 | console.WriteLine($" [ERROR] XProxy.Core.dll not exists in location {coreFile}"); 47 | return 1; 48 | } 49 | 50 | string targetCoreLocation = Path.Combine(MainPath, "XProxy.Core.dll"); 51 | 52 | var coreAssembly = Assembly.LoadFrom(coreFile); 53 | 54 | File.Move(coreFile, targetCoreLocation); 55 | 56 | string[] refs = 57 | { 58 | "Assembly-CSharp.dll", 59 | "BouncyCastle.Cryptography.dll", 60 | "Mirror.dll", 61 | "NorthwoodLib.dll", 62 | "UnityEngine.CoreModule.dll" 63 | }; 64 | 65 | List validReferences = new List(); 66 | 67 | foreach(var file in refs) 68 | { 69 | string targetPath = Path.Combine(SlReferences, file); 70 | 71 | if (!File.Exists(targetPath)) 72 | { 73 | console.WriteLine($" [WARN] Can't find reference at {targetPath}"); 74 | continue; 75 | } 76 | 77 | validReferences.Add(targetPath); 78 | } 79 | 80 | console.WriteLine($" [INFO] Create dependencies.zip archive with\n-{string.Join("\n -",validReferences)}"); 81 | 82 | using (ZipArchive archive = ZipFile.Open(Path.Combine(MainPath, "dependencies.zip"), ZipArchiveMode.Create)) 83 | { 84 | foreach (var file in validReferences) 85 | archive.CreateEntryFromFile(file, Path.GetFileName(file)); 86 | } 87 | 88 | console.WriteLine($" [INFO] Archive dependencies.zip created"); 89 | 90 | var type = coreAssembly.GetType("XProxy.Core.BuildInformation"); 91 | 92 | PropertyInfo textVersion = type.GetProperty("VersionText", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 93 | FieldInfo supportedGameVersions = type.GetField("SupportedGameVersions", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 94 | FieldInfo ChangelogsText = type.GetField("Changelogs", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); 95 | 96 | string version = (string) textVersion.GetValue(null); 97 | string[] changelogs = (string[])ChangelogsText.GetValue(null); 98 | string[] versions = (string[])supportedGameVersions.GetValue(null); 99 | 100 | BuildInfo bInfo = new BuildInfo() 101 | { 102 | DependenciesUrl = $"https://github.com/Killers0992/XProxy/releases/download/{version}/dependencies.zip", 103 | CoreUrl = $"https://github.com/Killers0992/XProxy/releases/download/{version}/XProxy.Core.dll", 104 | Changelogs = changelogs, 105 | Version = version, 106 | SupportedGameVersions = versions, 107 | }; 108 | 109 | string serialized = JsonConvert.SerializeObject(bInfo, Formatting.Indented); 110 | 111 | File.WriteAllText(Path.Combine(MainPath, "releaseinfo.json"), serialized); 112 | 113 | console.WriteLine($" [INFO] Serialized data to releaseinfo.json\n{serialized}"); 114 | 115 | return 0; 116 | } 117 | catch (Exception ex) 118 | { 119 | console.WriteLine(ex); 120 | return 1; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | /Matchmaker/Matchmaker.csproj 263 | /Matchmaker.API/Matchmaker.API.csproj 264 | /Matchmaker.API 265 | /Matchmaker 266 | -------------------------------------------------------------------------------- /XProxy.Core/Cryptography/ECDSA.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO; 4 | using System.Text; 5 | using Org.BouncyCastle.Crypto; 6 | using Org.BouncyCastle.Crypto.Generators; 7 | using Org.BouncyCastle.OpenSsl; 8 | using Org.BouncyCastle.Security; 9 | 10 | namespace XProxy.Cryptography 11 | { 12 | public static class ECDSA 13 | { 14 | /// 15 | /// Generates a ECDSA keys pair 16 | /// 17 | /// Length of the key 18 | /// Keys pair 19 | public static AsymmetricCipherKeyPair GenerateKeys(int size = 384) 20 | { 21 | var gen = new ECKeyPairGenerator("ECDSA"); 22 | var secureRandom = new SecureRandom(); 23 | var keyGenParam = new KeyGenerationParameters(secureRandom, size); 24 | gen.Init(keyGenParam); 25 | return gen.GenerateKeyPair(); 26 | } 27 | 28 | /// 29 | /// Signs data using ECDSA algorithm 30 | /// 31 | /// Data to sign 32 | /// Private key 33 | /// Signature 34 | public static string Sign(string data, AsymmetricKeyParameter privKey) 35 | { 36 | return Convert.ToBase64String(SignBytes(data, privKey)); 37 | } 38 | 39 | /// 40 | /// Signs data using ECDSA algorithm 41 | /// 42 | /// Data to sign 43 | /// Private key 44 | /// Signature 45 | public static byte[] SignBytes(string data, AsymmetricKeyParameter privKey) 46 | { 47 | try 48 | { 49 | byte[] buffer = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(data.Length)); 50 | int length = Utf8.GetBytes(data, buffer); 51 | byte[] result = SignBytes(buffer, 0, length, privKey); 52 | ArrayPool.Shared.Return(buffer); 53 | return result; 54 | } 55 | catch 56 | { 57 | return null; 58 | } 59 | } 60 | 61 | /// 62 | /// Signs data using ECDSA algorithm 63 | /// 64 | /// Data to sign 65 | /// Private key 66 | /// Signature 67 | public static byte[] SignBytes(byte[] data, AsymmetricKeyParameter privKey) 68 | { 69 | return SignBytes(data, 0, data.Length, privKey); 70 | } 71 | 72 | /// 73 | /// Signs data using ECDSA algorithm 74 | /// 75 | /// Data to sign 76 | /// Offset of the data 77 | /// Amount of bytes to sign 78 | /// Private key 79 | /// Signature 80 | public static byte[] SignBytes(byte[] data, int offset, int count, AsymmetricKeyParameter privKey) 81 | { 82 | try 83 | { 84 | var signer = SignerUtilities.GetSigner("SHA-256withECDSA"); 85 | signer.Init(true, privKey); 86 | signer.BlockUpdate(data, offset, count); 87 | var sigBytes = signer.GenerateSignature(); 88 | return sigBytes; 89 | } 90 | catch 91 | { 92 | return null; 93 | } 94 | } 95 | 96 | /// 97 | /// Verifies an ECDSA digital signature 98 | /// 99 | /// Signed data 100 | /// ECDSA signature 101 | /// Public key used to sign the data 102 | /// Whether the signature is valid or not 103 | public static bool Verify(string data, string signature, AsymmetricKeyParameter pubKey) 104 | { 105 | return VerifyBytes(data, Convert.FromBase64String(signature), pubKey); 106 | } 107 | 108 | /// 109 | /// Verifies an ECDSA digital signature 110 | /// 111 | /// Signed data 112 | /// ECDSA signature 113 | /// Public key used to sign the data 114 | /// Whether the signature is valid or not 115 | public static bool VerifyBytes(string data, byte[] signature, AsymmetricKeyParameter pubKey) 116 | { 117 | try 118 | { 119 | var signer = SignerUtilities.GetSigner("SHA-256withECDSA"); 120 | signer.Init(false, pubKey); 121 | byte[] buffer = ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxByteCount(data.Length)); 122 | int length = Utf8.GetBytes(data, buffer); 123 | signer.BlockUpdate(buffer, 0, length); 124 | ArrayPool.Shared.Return(buffer); 125 | return signer.VerifySignature(signature); 126 | } 127 | catch (Exception e) 128 | { 129 | Console.WriteLine("ECDSA Verification Error (BouncyCastle): " + e.Message + ", " + e.StackTrace); 130 | return false; 131 | } 132 | } 133 | 134 | /// 135 | /// Loads a public key from PEM 136 | /// 137 | /// PEM to process 138 | /// Loaded key 139 | public static AsymmetricKeyParameter PublicKeyFromString(string key) 140 | { 141 | // Disposed un-disposed StringReader @ Dankrushen 142 | using (TextReader reader = new StringReader(key)) 143 | { 144 | return (AsymmetricKeyParameter)new PemReader(reader).ReadObject(); 145 | } 146 | } 147 | 148 | /// 149 | /// Loads a private key from PEM 150 | /// 151 | /// PEM to process 152 | /// Loaded key 153 | public static AsymmetricKeyParameter PrivateKeyFromString(string key) 154 | { 155 | // Disposed un-disposed StringReader @ Dankrushen 156 | using (TextReader reader = new StringReader(key)) 157 | { 158 | return ((AsymmetricCipherKeyPair)new PemReader(reader).ReadObject()).Private; 159 | } 160 | } 161 | 162 | /// 163 | /// Saves an asymmetric key in a PEM format. 164 | /// 165 | /// Key to save 166 | /// PEM-encoded key 167 | public static string KeyToString(AsymmetricKeyParameter key) 168 | { 169 | // Disposed un-disposed StringWriter @ Dankrushen 170 | using (TextWriter textWriter = new StringWriter()) 171 | { 172 | var pemWriter = new PemWriter(textWriter); 173 | pemWriter.WriteObject(key); 174 | pemWriter.Writer.Flush(); 175 | return textWriter.ToString(); 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /XProxy.Core/Services/PluginsService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using XProxy.Core; 9 | 10 | namespace XProxy.Services 11 | { 12 | public class PluginsService 13 | { 14 | public List Dependencies = new List(); 15 | public Dictionary AssemblyToPlugin = new Dictionary(); 16 | 17 | string _pluginsPath => Path.Combine(ConfigService.MainDirectory, "Plugins"); 18 | string _dependenciesPath => Path.Combine(ConfigService.MainDirectory, "Dependencies"); 19 | 20 | IServiceCollection _serviceCollection; 21 | 22 | public PluginsService(IServiceCollection serviceCollection) 23 | { 24 | _serviceCollection = serviceCollection; 25 | 26 | if (!Directory.Exists(_pluginsPath)) 27 | Directory.CreateDirectory(_pluginsPath); 28 | 29 | if (!Directory.Exists(_dependenciesPath)) 30 | Directory.CreateDirectory(_dependenciesPath); 31 | 32 | LoadDependencies(); 33 | LoadPlugins(); 34 | } 35 | 36 | public void LoadDependencies() 37 | { 38 | string[] dependencies = Directory.GetFiles(_dependenciesPath, "*.dll"); 39 | 40 | int loaded = 0; 41 | 42 | for (int x = 0; x < dependencies.Length; x++) 43 | { 44 | Dependencies.Add(Assembly.LoadFrom(dependencies[x])); 45 | loaded++; 46 | } 47 | 48 | if (dependencies.Length == 0) 49 | { 50 | Logger.Info(ConfigService.Singleton.Messages.NoDependenciesToLoadMessage, "PluginsService"); 51 | } 52 | else 53 | { 54 | Logger.Info(ConfigService.Singleton.Messages.LoadedAllDependenciesMesssage.Replace("%loaded%", $"{loaded}"), "PluginsService"); 55 | } 56 | } 57 | 58 | public void LoadPlugins() 59 | { 60 | string[] plugins = Directory.GetFiles(_pluginsPath, "*.dll"); 61 | 62 | int loaded = 0; 63 | int failed = 0; 64 | 65 | for(int x = 0; x < plugins.Length; x++) 66 | { 67 | string name = Path.GetFileName(plugins[x]); 68 | 69 | if (name.StartsWith("-")) 70 | { 71 | Logger.Info(ConfigService.Singleton.Messages.PluginDisabledMessage.Replace("%current%", $"{x+1}").Replace("%max%", $"{plugins.Length}").Replace("%name%", name), "PluginsService"); 72 | continue; 73 | } 74 | 75 | byte[] data = File.ReadAllBytes(plugins[x]); 76 | Assembly assembly = Assembly.Load(data); 77 | 78 | Dictionary loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().Select(x => x.GetName()).ToDictionary(x => x.Name, y => y); 79 | Dictionary pluginReferences = assembly.GetReferencedAssemblies().ToDictionary(x => x.Name, y => y); 80 | 81 | var missingAssemblies = pluginReferences.Where(x => !loadedAssemblies.ContainsKey(x.Key)).ToList(); 82 | 83 | if (missingAssemblies.Count > 0) 84 | { 85 | StringBuilder sb = new StringBuilder(); 86 | sb.AppendLine(ConfigService.Singleton.Messages.PluginHasMissingDependenciesMessage.Replace("%current%", $"{x+1}").Replace("%max%", $"{plugins.Length}").Replace("%name%", name)); 87 | foreach(var dependency in missingAssemblies) 88 | { 89 | sb.AppendLine(ConfigService.Singleton.Messages.PluginMissingDependencyMessage.Replace("%name%", dependency.Key).Replace("%version%", dependency.Value.Version.ToString(3))); 90 | } 91 | Logger.Info(sb, "PluginsServices"); 92 | failed++; 93 | //continue; 94 | } 95 | 96 | Type[] types = assembly.GetTypes(); 97 | 98 | Plugin plugin = null; 99 | 100 | foreach (var type in types) 101 | { 102 | if (!type.IsSubclassOf(typeof(Plugin))) 103 | continue; 104 | 105 | plugin = (Plugin)Activator.CreateInstance(type); 106 | AssemblyToPlugin.Add(assembly, plugin); 107 | break; 108 | } 109 | 110 | if (plugin == null) 111 | { 112 | Logger.Info(ConfigService.Singleton.Messages.PluginInvalidEntrypointMessage.Replace("%current%", $"{x+1}").Replace("%max%", $"{plugins.Length}").Replace("%name%", name), "PluginsService"); 113 | failed++; 114 | continue; 115 | } 116 | 117 | name = plugin.Name; 118 | 119 | plugin.PluginDirectory = Path.Combine(_pluginsPath, $"{name}"); 120 | 121 | try 122 | { 123 | plugin.LoadConfig(); 124 | 125 | plugin.OnLoad(_serviceCollection); 126 | } 127 | catch (Exception ex) 128 | { 129 | Logger.Error($"Failed loading plugin {name}\n{ex}", "PluginsService"); 130 | } 131 | 132 | Logger.Info(ConfigService.Singleton.Messages.PluginLoadedMessage.Replace("%current%", $"{x+1}").Replace("%max%", $"{plugins.Length}").Replace("%name%", name), "PluginsService"); 133 | loaded++; 134 | } 135 | 136 | if (plugins.Length == 0) 137 | { 138 | Logger.Info(ConfigService.Singleton.Messages.NoPluginsToLoadMessage, "PluginsService"); 139 | } 140 | else if (plugins.Length == loaded) 141 | { 142 | Logger.Info(ConfigService.Singleton.Messages.LoadedAllPluginsMesssage.Replace("%loaded%", $"{loaded}"), "PluginsService"); 143 | } 144 | else 145 | { 146 | Logger.Info(ConfigService.Singleton.Messages.PluginsLoadedAndFailedToLoadMessage.Replace("%loaded%", $"{loaded}").Replace("%failed%", $"{failed}"), "PluginsService"); 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /XProxy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.31911.260 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XProxy.BuildListing", "XProxy.BuildListing\XProxy.BuildListing.csproj", "{9E9AC74C-4756-414B-813F-8C931332739D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XProxy.Core", "XProxy.Core\XProxy.Core.csproj", "{08F0C5AA-958D-428E-8BF7-5607A0398A8A}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XProxy", "XProxy\XProxy.csproj", "{EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XProxy.BuildRelease", "XProxy.BuildRelease\XProxy.BuildRelease.csproj", "{CC865614-D961-4CD6-A51D-E4751A15EED0}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XProxy.Plugin", "XProxy.Plugin\XProxy.Plugin.csproj", "{6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{6591D0A0-ECBA-4E55-868B-599583F5844D}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Release|Any CPU = Release|Any CPU 23 | Release|x64 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {9E9AC74C-4756-414B-813F-8C931332739D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {9E9AC74C-4756-414B-813F-8C931332739D}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {9E9AC74C-4756-414B-813F-8C931332739D}.Debug|x64.ActiveCfg = Debug|Any CPU 29 | {9E9AC74C-4756-414B-813F-8C931332739D}.Debug|x64.Build.0 = Debug|Any CPU 30 | {9E9AC74C-4756-414B-813F-8C931332739D}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {9E9AC74C-4756-414B-813F-8C931332739D}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {9E9AC74C-4756-414B-813F-8C931332739D}.Release|x64.ActiveCfg = Release|Any CPU 33 | {9E9AC74C-4756-414B-813F-8C931332739D}.Release|x64.Build.0 = Release|Any CPU 34 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Debug|x64.ActiveCfg = Debug|x64 37 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Debug|x64.Build.0 = Debug|x64 38 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Release|x64.ActiveCfg = Release|x64 41 | {08F0C5AA-958D-428E-8BF7-5607A0398A8A}.Release|x64.Build.0 = Release|x64 42 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Debug|x64.Build.0 = Debug|Any CPU 46 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Release|x64.ActiveCfg = Release|Any CPU 49 | {EEFD8D4C-9EC9-4A20-BDCC-0B2178CA1E7C}.Release|x64.Build.0 = Release|Any CPU 50 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Debug|x64.ActiveCfg = Debug|Any CPU 53 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Debug|x64.Build.0 = Debug|Any CPU 54 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Release|x64.ActiveCfg = Release|Any CPU 57 | {CC865614-D961-4CD6-A51D-E4751A15EED0}.Release|x64.Build.0 = Release|Any CPU 58 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Debug|x64.ActiveCfg = Debug|Any CPU 61 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Debug|x64.Build.0 = Debug|Any CPU 62 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Release|x64.ActiveCfg = Release|Any CPU 65 | {6B2AC35D-6DF4-4EA9-B83D-2B453D9F459F}.Release|x64.Build.0 = Release|Any CPU 66 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Debug|x64.ActiveCfg = Debug|Any CPU 69 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Debug|x64.Build.0 = Debug|Any CPU 70 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Release|x64.ActiveCfg = Release|Any CPU 73 | {03E1B8E8-7BED-4D14-99B9-346392298394}.Release|x64.Build.0 = Release|Any CPU 74 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 75 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 76 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Debug|x64.ActiveCfg = Debug|Any CPU 77 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Debug|x64.Build.0 = Debug|Any CPU 78 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Release|x64.ActiveCfg = Release|Any CPU 81 | {2B258C11-7D56-4A46-BC3B-48334B1376D0}.Release|x64.Build.0 = Release|Any CPU 82 | EndGlobalSection 83 | GlobalSection(SolutionProperties) = preSolution 84 | HideSolutionNode = FALSE 85 | EndGlobalSection 86 | GlobalSection(NestedProjects) = preSolution 87 | {03E1B8E8-7BED-4D14-99B9-346392298394} = {6591D0A0-ECBA-4E55-868B-599583F5844D} 88 | {2B258C11-7D56-4A46-BC3B-48334B1376D0} = {6591D0A0-ECBA-4E55-868B-599583F5844D} 89 | EndGlobalSection 90 | GlobalSection(ExtensibilityGlobals) = postSolution 91 | SolutionGuid = {A5C3C721-064E-4FD4-9D91-2BCE704BF8DD} 92 | EndGlobalSection 93 | EndGlobal 94 | -------------------------------------------------------------------------------- /XProxy.Core/Core/Connections/LobbyConnection.cs: -------------------------------------------------------------------------------- 1 | using Mirror; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using XProxy.Core.Core.Connections.Responses; 5 | using XProxy.Services; 6 | 7 | namespace XProxy.Core.Connections 8 | { 9 | public class LobbyConnection : SimulatedConnection 10 | { 11 | public const ushort NoclipToggleId = 48298; 12 | public const ushort VoiceMessageId = 41876; 13 | 14 | public List Servers; 15 | public int CurrentIndex; 16 | 17 | public bool Connect; 18 | public bool IsConnecting; 19 | 20 | public string InfoMessage; 21 | public int InfoMessageDuration; 22 | 23 | public LobbyConnection(Player plr) : base(plr) 24 | { 25 | Servers = Server.GetServerNames(Player); 26 | } 27 | 28 | public override void OnConnected() 29 | { 30 | Logger.Info(ConfigService.Singleton.Messages.LobbyConnectedMessage.Replace("%tag%", Player.Tag).Replace("%address%", $"{Player.ClientEndPoint}").Replace("%userid%", Player.UserId), $"Player"); 31 | Player.SendToScene("Facility"); 32 | } 33 | 34 | public override void OnClientReady() 35 | { 36 | Player.ObjectSpawnStarted(); 37 | Player.ObjectSpawnFinished(); 38 | } 39 | 40 | public override void OnAddPlayer() 41 | { 42 | Player.Spawn(); 43 | Player.SetRole(PlayerRoles.RoleTypeId.Tutorial); 44 | Player.SetHealth(100f); 45 | } 46 | 47 | public void ShowInfo(string message, int duration) 48 | { 49 | InfoMessage = message; 50 | InfoMessageDuration = duration; 51 | } 52 | 53 | public override void Update() 54 | { 55 | if (InfoMessageDuration != 0) 56 | InfoMessageDuration--; 57 | 58 | if (Connect && !IsConnecting) 59 | { 60 | string serverName = Servers[CurrentIndex]; 61 | 62 | if (!Server.TryGetByName(serverName, out Server server)) 63 | { 64 | ShowInfo($"Server {serverName} not found!", 3); 65 | Connect = false; 66 | return; 67 | } 68 | 69 | Player.ConnectTo(server); 70 | IsConnecting = true; 71 | } 72 | 73 | SendInfoHint(); 74 | } 75 | 76 | public override void OnConnectionResponse(Server server, BaseResponse response) 77 | { 78 | switch (response) 79 | { 80 | case ServerIsFullResponse _: 81 | ShowInfo($"Server {server.Name} is full!", 3); 82 | break; 83 | case ServerIsOfflineResponse _: 84 | ShowInfo($"Server {server.Name} is offline!", 3); 85 | break; 86 | } 87 | 88 | IsConnecting = false; 89 | Connect = false; 90 | } 91 | 92 | public void SendInfoHint(float dur = 1.2f) 93 | { 94 | HintBuilder builder = new HintBuilder(); 95 | 96 | int startingLine = 12; 97 | 98 | startingLine = startingLine - (Server.List.Count >= 12 ? 0 : Server.List.Count); 99 | 100 | builder.SetRightLine(startingLine, " Servers ‎‎"); 101 | 102 | for(int x = 0; x < 16; x++) 103 | { 104 | Server serv = Server.List.ElementAtOrDefault(x); 105 | 106 | if (serv == null) 107 | break; 108 | 109 | if (serv == Player.CurrentServer) 110 | continue; 111 | 112 | int serverIndex = Servers.IndexOf(serv.Name); 113 | 114 | startingLine++; 115 | builder.SetRightLine(startingLine, $"{(serverIndex == CurrentIndex ? ">" : string.Empty)} {serv.Name} {serv.PlayersCount}/{serv.Settings.MaxPlayers} ["); 116 | } 117 | 118 | startingLine++; 119 | builder.SetRightLine(startingLine, $" Total players connected {Player.Count} ‎‎"); 120 | startingLine += 2; 121 | builder.SetRightLine(startingLine, $"Switch server by pressing Noclip Keybind"); 122 | 123 | if (InfoMessageDuration > 0) 124 | builder.SetCenterLine(15, InfoMessage); 125 | 126 | builder.SetCenterLine(23, $"You will be connecting to server {Servers[CurrentIndex]}"); 127 | builder.SetCenterLine(24, $"Press Voicechat Keybind to connect"); 128 | 129 | Player.SendHint(builder.ToString(), dur); 130 | } 131 | 132 | public override void OnReceiveGameConsoleCommand(string command, string[] args) 133 | { 134 | switch (command.ToLower()) 135 | { 136 | case "gsh": 137 | Player.SendGameConsoleMessage("Syntax: .connect IP:PORT", "green"); 138 | break; 139 | case "servers": 140 | string text = $"Servers: " + string.Join(", ", Server.List.Select(x => $"{x.Name} ({x.PlayersCount}/{x.Settings.MaxPlayers})")); 141 | Player.SendGameConsoleMessage(text, "green"); 142 | break; 143 | case "connect": 144 | if (args.Length == 0 || string.IsNullOrEmpty(args[0])) 145 | { 146 | Player.SendGameConsoleMessage("Syntax: connect ", "red"); 147 | return; 148 | } 149 | 150 | Server serv; 151 | if (args[0].Contains(":")) 152 | { 153 | if (!Server.TryGetByPublicIp(args[0], out serv)) 154 | { 155 | Player.SendGameConsoleMessage($"Server with ip {args[0]} not found!", "red"); 156 | return; 157 | } 158 | } 159 | else 160 | { 161 | if (!Server.TryGetByName(args[0], out serv)) 162 | { 163 | Player.SendGameConsoleMessage($"Server with name {args[0]} not found!", "red"); 164 | return; 165 | } 166 | } 167 | 168 | Player.ConnectTo(serv); 169 | Player.SendGameConsoleMessage($"Connecting to {args[0]}...", "green"); 170 | break; 171 | } 172 | } 173 | 174 | public override void OnReceiveMirrorDataFromProxy(uint key, NetworkReader reader) 175 | { 176 | if (key == 3034 || key == 53182) 177 | return; 178 | 179 | switch (key) 180 | { 181 | case NoclipToggleId: 182 | CurrentIndex = Servers.Count <= CurrentIndex + 1 ? 0 : CurrentIndex + 1; 183 | break; 184 | case VoiceMessageId: 185 | Connect = true; 186 | break; 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /XProxy.Core/Models/PreAuthModel.cs: -------------------------------------------------------------------------------- 1 | using CentralAuth; 2 | using LiteNetLib.Utils; 3 | using Org.BouncyCastle.Asn1.Cmp; 4 | using System; 5 | using System.Text; 6 | using XProxy.Core; 7 | using XProxy.Cryptography; 8 | using XProxy.Enums; 9 | using XProxy.Services; 10 | 11 | namespace XProxy.Models 12 | { 13 | public class PreAuthModel 14 | { 15 | public NetDataWriter RawPreAuth; 16 | public Server Server; 17 | 18 | public static PreAuthModel ReadPreAuth(string endpoint, NetDataReader reader, ref string failedOn) 19 | { 20 | PreAuthModel model = new PreAuthModel(); 21 | model.IpAddress = endpoint; 22 | 23 | model.RawPreAuth = NetDataWriter.FromBytes(reader.RawData, reader.UserDataOffset, reader.UserDataSize); 24 | 25 | failedOn = "Client Type"; 26 | 27 | if (!reader.TryGetByte(out byte clientType)) 28 | return model; 29 | 30 | model.ClientType = (ClientType)clientType; 31 | 32 | if (model.ClientType == ClientType.Proxy) 33 | { 34 | if (!reader.TryGetString(out string connectionKey)) 35 | return model; 36 | 37 | if (!reader.TryGetUShort(out ushort port)) 38 | return model; 39 | 40 | if (!Server.TryGetByIp(endpoint, port, out Server server)) 41 | return model; 42 | 43 | if (connectionKey != server.Settings.PluginExtension.ConnectionKey) 44 | return model; 45 | 46 | // If allowed connections contains any defined ip check if incoming endpoint is in that list. 47 | if (server.Settings.PluginExtension.AllowedConnections.Length > 0) 48 | { 49 | if (!server.Settings.PluginExtension.AllowedConnections.Contains(endpoint)) 50 | return null; 51 | } 52 | 53 | model.Server = server; 54 | return model; 55 | } 56 | 57 | failedOn = "Major Version"; 58 | if (!reader.TryGetByte(out byte major)) return model; 59 | model.Major = major; 60 | 61 | failedOn = "Minor Version"; 62 | if (!reader.TryGetByte(out byte minor)) return model; 63 | model.Minor = minor; 64 | 65 | failedOn = "Revision Version"; 66 | if (!reader.TryGetByte(out byte revision)) return model; 67 | model.Revision = revision; 68 | 69 | failedOn = "Backward Compatibility"; 70 | if (!reader.TryGetBool(out bool backwardCompatibility)) return model; 71 | model.BackwardCompatibility = backwardCompatibility; 72 | 73 | if (backwardCompatibility) 74 | { 75 | failedOn = "Backward Revision"; 76 | if (!reader.TryGetByte(out byte backwardRevision)) return model; 77 | model.BackwardRevision = backwardRevision; 78 | } 79 | 80 | failedOn = "ChallengeID"; 81 | if (!reader.TryGetInt(out int challengeid)) return model; 82 | model.ChallengeID = challengeid; 83 | 84 | if (model.ChallengeID != 0) 85 | { 86 | failedOn = "ChallengeResponse"; 87 | if (!reader.TryGetBytesWithLength(out byte[] challenge)) return model; 88 | model.ChallengeResponse = challenge; 89 | } 90 | else 91 | model.ChallengeResponse = new byte[0]; 92 | 93 | failedOn = "UserID"; 94 | if (!reader.TryGetString(out string userid)) return model; 95 | 96 | failedOn = "UserID is null/empty ( player not authenticated )"; 97 | if (string.IsNullOrEmpty(userid)) return model; 98 | model.UserID = userid; 99 | 100 | failedOn = "Expiration"; 101 | if (!reader.TryGetLong(out long expiration)) return model; 102 | model.Expiration = expiration; 103 | 104 | failedOn = "Expiration check"; 105 | if (DateTimeOffset.UtcNow.ToUnixTimeSeconds() > expiration) return model; 106 | 107 | failedOn = "Flags"; 108 | if (!reader.TryGetByte(out byte flags)) return model; 109 | model.Flags = (CentralAuthPreauthFlags)flags; 110 | 111 | failedOn = "Region"; 112 | if (!reader.TryGetString(out string region)) return model; 113 | model.Country = region; 114 | 115 | failedOn = "Signature"; 116 | if (!reader.TryGetBytesWithLength(out byte[] signature)) return model; 117 | model.Signature = signature; 118 | 119 | failedOn = "Signature check"; 120 | if (!ECDSA.VerifyBytes($"{userid};{flags};{region};{expiration}", signature, PublicKeyService.PublicKey)) 121 | return model; 122 | 123 | model.IsValid = true; 124 | return model; 125 | } 126 | 127 | public bool IsValid { get; set; } 128 | public string IpAddress { get; set; } 129 | public ClientType ClientType { get; set; } 130 | public byte Major { get; set; } 131 | public byte Minor { get; set; } 132 | public byte Revision { get; set; } 133 | public string Version => $"{Major}.{Minor}.{Revision}"; 134 | public bool BackwardCompatibility { get; set; } 135 | public byte BackwardRevision { get; set; } 136 | 137 | public int ChallengeID { get; set; } 138 | public byte[] ChallengeResponse { get; set; } 139 | 140 | public string UserID { get; set; } = "Unknown UserID"; 141 | 142 | public long Expiration { get; set; } 143 | 144 | public CentralAuthPreauthFlags Flags { get; set; } 145 | 146 | public string Country { get; set; } = "Unknown Country"; 147 | 148 | public byte[] Signature { get; set; } = new byte[0]; 149 | 150 | public NetDataWriter Create(bool includeIp) 151 | { 152 | NetDataWriter writer = new NetDataWriter(); 153 | 154 | writer.Put((byte)ClientType.GameClient); 155 | writer.Put(Major); 156 | writer.Put(Minor); 157 | writer.Put(Revision); 158 | writer.Put(BackwardCompatibility); 159 | 160 | if (BackwardCompatibility) 161 | writer.Put(BackwardRevision); 162 | 163 | writer.Put(0); 164 | 165 | writer.Put(UserID); 166 | writer.Put(Expiration); 167 | writer.Put((byte)Flags); 168 | writer.Put(Country); 169 | writer.PutBytesWithLength(Signature); 170 | 171 | if (includeIp) 172 | writer.Put(IpAddress); 173 | 174 | return writer; 175 | } 176 | 177 | public NetDataWriter CreateChallenge(int challengeId, byte[] challengeResponse, bool includeIp) 178 | { 179 | NetDataWriter writer = new NetDataWriter(); 180 | 181 | writer.Put((byte)ClientType.GameClient); 182 | writer.Put(Major); 183 | writer.Put(Minor); 184 | writer.Put(Revision); 185 | writer.Put(BackwardCompatibility); 186 | 187 | if (BackwardCompatibility) 188 | writer.Put(BackwardRevision); 189 | 190 | writer.Put(challengeId); 191 | writer.PutBytesWithLength(challengeResponse); 192 | 193 | writer.Put(UserID); 194 | writer.Put(Expiration); 195 | writer.Put((byte)Flags); 196 | writer.Put(Country); 197 | writer.PutBytesWithLength(Signature); 198 | 199 | if (includeIp) 200 | writer.Put(IpAddress); 201 | 202 | return writer; 203 | } 204 | 205 | public override string ToString() 206 | { 207 | return string.Concat( 208 | $"Client Type: {ClientType}", 209 | Environment.NewLine, 210 | $"Version: {Major}.{Minor}.{Revision}, Backward Compatibility: {(BackwardCompatibility ? "NO" : $"YES ( Revision {BackwardRevision} )")}", 211 | Environment.NewLine, 212 | $"Challenge ID: {ChallengeID}", 213 | Environment.NewLine, 214 | $"Challenge: {Encoding.UTF8.GetString(ChallengeResponse)}", 215 | Environment.NewLine, 216 | $"UserID: {UserID}", 217 | Environment.NewLine, 218 | $"Expiration: {Expiration}", 219 | Environment.NewLine, 220 | $"Flags: {Flags}", 221 | Environment.NewLine, 222 | $"Region: {Country}", 223 | Environment.NewLine, 224 | $"Signature length: {Signature.Length}"); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /XProxy.Core/Models/MessagesModel.cs: -------------------------------------------------------------------------------- 1 | namespace XProxy.Models 2 | { 3 | public class MessagesModel 4 | { 5 | public string ProxyVersion { get; set; } = "Running version (f=green)%version%(f=white), supported game versions (f=green)%gameVersion%(f=white)"; 6 | public string PlayerTag { get; set; } = "(f=white)[(f=darkcyan)%serverIpPort%(f=white)] [(f=cyan)%server%(f=white)]"; 7 | public string PlayerErrorTag { get; set; } = "(f=red)[(f=darkcyan)%serverIpPort%(f=red)] [(f=cyan)%server%(f=red)]"; 8 | public string Proxy { get; set; } = "proxy"; 9 | public string CurrentServer { get; set; } = "current server"; 10 | public string ServerIsOfflineKickMessage { get; set; } = "Server %server% is offline!"; 11 | public string ProxyStartedListeningMessage { get; set; } = "Listening on (f=green)%ip%:%port%(f=white), accepting clients with game version (f=green)%version%(f=white)"; 12 | public string ProxyClientClosedConnectionMessage { get; set; } = "%tag% Client (f=green)%address%(f=white) ((f=green)%userid%(f=white)) closed connection."; 13 | public string ProxyClientDisconnectedWithReasonMessage { get; set; } = "%tag% Client (f=green)%address%(f=white) ((f=green)%userid%(f=white)) disconnected with reason (f=green)%reason%(f=white)."; 14 | public string CommandRegisteredMessage { get; set; } = "Command (f=green)%name%(f=white) registered!"; 15 | public string CommandAlreadyRegisteredMessage { get; set; } = "Command (f=green)%name%(f=yellow) is already registered!"; 16 | public string CommandNotExistsMessage { get; set; } = "Unknown command %name%, type \"help\""; 17 | public string ConfigLoadedMessage { get; set; } = "Loaded config!"; 18 | public string ConfigSavedMessage { get; set; } = "Saved config!"; 19 | public string TokenLoadedMessage { get; set; } = "Verification token loaded! Server probably will be listed on public list."; 20 | public string TokenReloadedMessage { get; set; } = "Verification token reloaded."; 21 | public string CentralCommandMessage { get; set; } = "[(f=green)%command%(f=white)] %message%"; 22 | public string VerificationChallengeObtainedMessage { get; set; } = "Verification challenge and response have been obtained."; 23 | public string FailedToUpdateMessage { get; set; } = "Could not update server data on server list - %error%"; 24 | public string ReceivedTokenMessage { get; set; } = "Received verification token from central server."; 25 | public string MessageFromCentralsMessage { get; set; } = "[(f=green)MESSAGE FROM CENTRAL SERVER(f=white)] %message%"; 26 | public string TokenSavedMessage { get; set; } = "New verification token saved."; 27 | public string TokenFailedToSaveMessage { get; set; } = "New verification token could not be saved: (f=green)%error%(f=red)"; 28 | public string PasswordSavedMessage { get; set; } = "New password saved."; 29 | public string PasswordFailedToSaveMessage { get; set; } = "New password could not be saved."; 30 | public string CantUpdateDataMessage { get; set; } = "Could not update data on server list."; 31 | public string CantRefreshPublicKeyMessage { get; set; } = "Can't refresh central server public key - invalid signature!"; 32 | public string CantRefreshPublicKey2Message { get; set; } = "Can't refresh central server public key - %message% %response%!"; 33 | public string ObtainedPublicKeyMessage { get; set; } = "Downloaded public key from central server."; 34 | public string ServerListedMessage { get; set; } = "Server is listed on serverlist!"; 35 | public string NoDependenciesToLoadMessage { get; set; } = "No dependencies to load."; 36 | public string LoadedAllDependenciesMesssage { get; set; } = "Successfully loaded all (f=green)%loaded%(f=white) dependencies!"; 37 | public string PluginDisabledMessage { get; set; } = "[(f=darkcyan)%current%/%max%(f=white)] Plugin (f=green)%name%(f=white) is disabled!"; 38 | public string PluginHasMissingDependenciesMessage { get; set; } = "[(f=darkcyan)%current%/%max%(f=white)] Plugin (f=green)%name%(f=white) failed to load, missing dependencies"; 39 | public string PluginMissingDependencyMessage { get; set; } = " - (f=green)%name%(f=white) %version%"; 40 | public string PluginInvalidEntrypointMessage { get; set; } = "[(f=darkcyan)%current%/%max%(f=white)] Plugin (f=green)%name%(f=white) don't have any valid entrypoints!"; 41 | public string PluginLoadedMessage { get; set; } = "[(f=darkcyan)%current%/%max%(f=white)] Plugin (f=green)%name%(f=white) loaded!"; 42 | public string NoPluginsToLoadMessage { get; set; } = "No plugins to load."; 43 | public string LoadedAllPluginsMesssage { get; set; } = "Successfully loaded all (f=green)%loaded%(f=white) plugins!"; 44 | public string PluginsLoadedAndFailedToLoadMessage { get; set; } = "Loaded (f=green)%loaded%(f=white) plugins and (f=red)%failed%(f=white) failed to load!"; 45 | public string LoadedPublicKeyFromCache { get; set; } = "Loaded central server public key from cache."; 46 | public string DownloadPublicKeyFromCentrals { get; set; } = "Downloading public key from central server..."; 47 | public string PreAuthIsInvalidMessage { get; set; } = $"Preauth is invalid for connection (f=green)%address%(f=yellow) failed on (f=green)%failed%(f=yellow)"; 48 | public string MaintenanceDisconnectMessage { get; set; } = "Client (f=green)%address%(f=white) ((f=green)%userid%(f=white)) server is (f=yellow)under maintenance(f=white)!"; 49 | public string PlayerRedirectToMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) redirect to (f=green)%server%(f=white)!"; 50 | public string PlayerRoundRestartMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) roundrestart, time %time%."; 51 | public string PlayerSentChallengeMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) sent (f=green)challenge(f=white)!"; 52 | public string PlayerIsConnectingMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) is connecting..."; 53 | public string PlayerConnectedMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) connected!"; 54 | public string PlayerNetworkExceptionMessage { get; set; } = "%tag% Exception while updating network for client (f=green)%address%(f=red) ((f=green)%userid%(f=red)), %message%"; 55 | public string PlayerUnbatchingExceptionMessage { get; set; } = "%tag% Exception while unbatching message for (f=green)%address%(f=red) ((f=green)%userid%(f=red)) from %condition%, %message%"; 56 | public string PlayerExceptionSendToProxyMessage { get; set; } = "%tag% Exception while sending message to proxy for (f=green)%address%(f=red) ((f=green)%userid%(f=red)), %message%"; 57 | public string PlayerExceptionSendToServerMessage { get; set; } = "%tag% Exception while sending message to current server for (f=green)%address%(f=red) ((f=green)%userid%(f=red)), %message%"; 58 | public string PlayerServerIsOfflineMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) server is offline!"; 59 | public string PlayerDelayedConnectionMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) delayed for (f=green)%time%(f=white) seconds."; 60 | public string PlayerServerIsFullMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) server is full."; 61 | public string PlayerBannedMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%useid%(f=white)) banned for (f=red)%reason%(f=white), expires (f=red)%date% %time%(f=white)."; 62 | public string PlayerReceivedChallengeMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) received (f=green)challenge(f=white)."; 63 | public string PlayerDisconnectedMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) disconnected, reason (f=green)%reason%(f=white)"; 64 | public string PlayerDisconnectedWithReasonMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) disconnected, reason (f=green)%reason%(f=white)"; 65 | public string PlayerServerTimeoutMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) server timeout."; 66 | public string PlayerServerShutdownMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) server shutdown."; 67 | public string LobbyConnectedMessage { get; set; } = "%tag% (f=green)%address%(f=white) ((f=green)%userid%(f=white)) connected to lobby!"; 68 | public string LobbyConnectingToServerHint { get; set; } = "Connecting to %server%..."; 69 | public string[] LobbyMainHint { get; set; } = new string[] 70 | { 71 | "LOBBY", 72 | "%proxyOnlinePlayers%/%proxyMaxPlayers%", 73 | "", 74 | "Servers", 75 | "%serversLine1%", 76 | "%serversLine2%", 77 | "", 78 | "You will join server %server%", 79 | "", 80 | "Press LeftAlt to change server which you want to join, hold Q to join." 81 | }; 82 | public string LobbyServerLine1 { get; set; } = "%server%"; 83 | public string SelectedServerColor { get; set; } = "green"; 84 | public string DefaultServerColor { get; set; } = "white"; 85 | public string LobbyServerLine2 { get; set; } = "%onlinePlayers%/%maxPlayers%"; 86 | public string PositionInQueue { get; set; } = "Position %position%/%totalInQueue%"; 87 | public string FirstPositionInQueue { get; set; } = "Position %position%/%totalInQueue%"; 88 | public string LostConnectionHint { get; set; } = "XProxy\nServer is not responding for %time% seconds..."; 89 | public string SearchingForFallbackServerHint { get; set; } = "XProxy\nSearching for fallback server..."; 90 | public string OnlineServerNotFoundHint { get; set; } = "XProxy\nCan't find any online servers, disconnecting..."; 91 | public string ConnectingToServerHint { get; set; } = "XProxy\nConnecting to server %server%..."; 92 | public string MaintenanceKickMessage { get; set; } = "Server is under maintenance!"; 93 | public string ProxyIsFull { get; set; } = "Client (f=green)%address%(f=white) ((f=green)%userid%(f=white)) proxy is full!"; 94 | public string WrongVersion { get; set; } = "Client (f=green)%address%(f=white) ((f=green)%userid%(f=white)) tried joining proxy with wrong version (f=green)%version%(f=white)!"; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /XProxy/Services/UpdaterService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Newtonsoft.Json; 3 | using System.IO.Compression; 4 | using System.Reflection; 5 | using System.Security.Cryptography; 6 | using XProxy.Misc; 7 | using XProxy.Models; 8 | 9 | namespace XProxy.Services 10 | { 11 | public class UpdaterService : BackgroundService 12 | { 13 | private static bool _uptodateNotice; 14 | private static HttpClient _client = new HttpClient(); 15 | private static BuildInfo _latestBuild; 16 | 17 | public const string BuildsProviderUrl = "https://killers0992.github.io/XProxy/builds.json"; 18 | public const string VersionsUrls = "https://raw.githubusercontent.com/Killers0992/XProxy/master/Storage/gameVersions.json"; 19 | 20 | public const int CheckUpdatesEveryMs = 30000; 21 | 22 | public static Version InstalledVersion; 23 | public static string[] RemoteGameVersions; 24 | 25 | public static string DependenciesFolder => Path.Combine(Environment.CurrentDirectory, "Dependencies"); 26 | public static string ProxyFile => Path.Combine(Environment.CurrentDirectory, "XProxy.Core.dll"); 27 | 28 | public static void FetchVersion() 29 | { 30 | if (!File.Exists(ProxyFile)) 31 | { 32 | InstalledVersion = new Version(0,0,0); 33 | return; 34 | } 35 | 36 | AssemblyName name = AssemblyName.GetAssemblyName(ProxyFile); 37 | 38 | if (name == null) 39 | { 40 | InstalledVersion = new Version(0, 0, 0); 41 | return; 42 | } 43 | 44 | InstalledVersion = name.Version; 45 | } 46 | 47 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 48 | { 49 | while(!stoppingToken.IsCancellationRequested) 50 | { 51 | await Task.Delay(CheckUpdatesEveryMs); 52 | 53 | try 54 | { 55 | await CheckUpdates(); 56 | } 57 | catch (Exception ex) 58 | { 59 | ConsoleLogger.Error($"Failed checking updates {ex}", "XProxy"); 60 | } 61 | } 62 | } 63 | 64 | public static async Task IntialRun() 65 | { 66 | FetchVersion(); 67 | await CheckUpdates(true); 68 | } 69 | 70 | public static async Task CheckUpdates(bool isIntial = false) 71 | { 72 | using (var response = await _client.GetAsync(VersionsUrls)) 73 | { 74 | string textResponse = await response.Content.ReadAsStringAsync(); 75 | 76 | string[] versions = JsonConvert.DeserializeObject(textResponse); 77 | 78 | if (versions != null) 79 | RemoteGameVersions = versions; 80 | } 81 | 82 | BuildsListing listing = null; 83 | try 84 | { 85 | listing = await FetchBuilds(); 86 | } 87 | catch (HttpRequestException ex) 88 | { 89 | ConsoleLogger.Error($"Failed to fetch builds ({(ex.StatusCode.HasValue ? ex.StatusCode.Value : $"Website Down")}) !", "XProxy"); 90 | } 91 | 92 | if (listing == null) 93 | { 94 | ConsoleLogger.Error($"Fetched listing is invalid!", "XProxy"); 95 | return; 96 | } 97 | 98 | BuildInfo[] builds = listing.Builds 99 | .Where(x => x.Value.ParsedVersion.CompareTo(InstalledVersion) > 0 && x.Value.SupportedGameVersions.Contains(LauncherSettings.Value.GameVersion.ToUpper() == "LATEST" ? RemoteGameVersions[0] : LauncherSettings.Value.GameVersion)) 100 | .OrderByDescending(x => x.Value.ParsedVersion) 101 | .Select(x => x.Value) 102 | .ToArray(); 103 | 104 | if (builds.Length == 0) 105 | { 106 | if (!_uptodateNotice && InstalledVersion.Major != 0) 107 | { 108 | ConsoleLogger.Info("Proxy is up to date!", "XProxy"); 109 | _uptodateNotice = true; 110 | } 111 | } 112 | else 113 | { 114 | BuildInfo latest = builds.FirstOrDefault(); 115 | 116 | if (latest != _latestBuild) 117 | { 118 | if (InstalledVersion.Major == 0) 119 | ConsoleLogger.Info($"Installing (f=cyan)XProxy(f=white) ((f=green){latest.ParsedVersion.ToString(3)}(f=white))", "XProxy"); 120 | else 121 | ConsoleLogger.Info($"New version of (f=cyan)XProxy(f=white) found, (f=darkgreen){InstalledVersion.ToString(3)}(f=white) => (f=green){latest.ParsedVersion.ToString(3)}(f=white)", "XProxy"); 122 | 123 | if (latest.Changelogs.Length == 0) 124 | ConsoleLogger.Info("Changelogs not found...", "XProxy"); 125 | else 126 | { 127 | ConsoleLogger.Info("Changelogs:", "XProxy"); 128 | foreach (var changelog in latest.Changelogs) 129 | { 130 | ConsoleLogger.Info($" - (f=green){changelog}(f=white)"); 131 | } 132 | } 133 | 134 | _latestBuild = latest; 135 | } 136 | 137 | if (isIntial) 138 | { 139 | if (LauncherSettings.Value.DisableUpdater) 140 | { 141 | ConsoleLogger.Warn($"Updater is disabled, skipping build download!", "XProxy"); 142 | } 143 | else 144 | await DownloadBuild(_latestBuild); 145 | } 146 | } 147 | } 148 | 149 | private static async Task FetchBuilds() 150 | { 151 | using (HttpResponseMessage response = await _client.GetAsync(BuildsProviderUrl)) 152 | { 153 | string textResponse = await response.Content.ReadAsStringAsync(); 154 | 155 | try 156 | { 157 | BuildsListing listing = JsonConvert.DeserializeObject(textResponse); 158 | return listing; 159 | } 160 | catch(Exception ex) 161 | { 162 | ConsoleLogger.Error($"Failed deserializing builds listing! {ex}", "XProxy"); 163 | return null; 164 | } 165 | } 166 | } 167 | 168 | private static async Task DownloadBuild(BuildInfo build) 169 | { 170 | using (FileStream proxyStream = new FileStream(ProxyFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) 171 | { 172 | CustomProgressReporter reporter = new CustomProgressReporter("Downloading (f=cyan)XProxy.Core.dll(f=white) (f=green)%percentage%%(f=white)...", null); 173 | 174 | await _client.DownloadAsync(build.CoreUrl, proxyStream, reporter); 175 | } 176 | 177 | FetchVersion(); 178 | ConsoleLogger.Info($"Downloaded (f=cyan)XProxy.Core.dll(f=white) ((f=green){InstalledVersion.ToString(3)}(f=white))", "XProxy"); 179 | 180 | string _dependencies = "./_dependencies.zip"; 181 | 182 | if (!Directory.Exists(DependenciesFolder)) 183 | Directory.CreateDirectory(DependenciesFolder); 184 | 185 | using (FileStream depsStream = new FileStream(_dependencies, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) 186 | { 187 | CustomProgressReporter reporter = new CustomProgressReporter("Downloading dependencies (f=green)%percentage%%(f=white)...", null); 188 | 189 | await _client.DownloadAsync(build.DependenciesUrl, depsStream, reporter); 190 | 191 | using (ZipArchive depsZip = new ZipArchive(depsStream)) 192 | { 193 | Dictionary dependenciesHashes = GetDependenciesHashes(); 194 | List dependenciesToRemove = dependenciesHashes.Values.ToList(); 195 | 196 | MD5 md5 = MD5.Create(); 197 | 198 | foreach (ZipArchiveEntry entry in depsZip.Entries) 199 | { 200 | byte[] buffer = new byte[16*1024]; 201 | 202 | byte[] data = null; 203 | 204 | Stream stream = entry.Open(); 205 | 206 | using (MemoryStream ms = new MemoryStream()) 207 | { 208 | int read; 209 | while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) 210 | { 211 | ms.Write(buffer, 0, read); 212 | } 213 | 214 | data = ms.ToArray(); 215 | } 216 | 217 | string entryHash = Convert.ToBase64String(md5.ComputeHash(data)); 218 | 219 | string entryName = entry.Name; 220 | 221 | if (dependenciesHashes.TryGetValue(entryName, out string depHash)) 222 | { 223 | if (depHash == entryHash) 224 | { 225 | // This dependency is uptodate! skip 226 | dependenciesToRemove.Remove(entryName); 227 | continue; 228 | } 229 | else 230 | { 231 | // This dependency neets to be updated! 232 | File.WriteAllBytes(Path.Combine(DependenciesFolder, entryName), data); 233 | dependenciesToRemove.Remove(entryName); 234 | ConsoleLogger.Info($"Update dependency (f=cyan){entryName}(f=white)", "XProxy"); 235 | } 236 | } 237 | else 238 | { 239 | // This dependency not exists, create one! 240 | File.WriteAllBytes(Path.Combine(DependenciesFolder, entryName), data); 241 | ConsoleLogger.Info($"Download dependency (f=cyan){entryName}(f=white)", "XProxy"); 242 | } 243 | } 244 | } 245 | } 246 | 247 | ConsoleLogger.Info($"Downloaded (f=cyan)XProxy(f=white) dependencies", "XProxy"); 248 | } 249 | 250 | private static Dictionary GetDependenciesHashes() 251 | { 252 | MD5 md5 = MD5.Create(); 253 | 254 | Dictionary hashes = new Dictionary(); 255 | 256 | foreach (var file in Directory.GetFiles(DependenciesFolder)) 257 | { 258 | byte[] hashAlgo = md5.ComputeHash(File.ReadAllBytes(file)); 259 | 260 | string name = Path.GetFileName(file); 261 | 262 | hashes[name] = Convert.ToBase64String(hashAlgo); 263 | } 264 | 265 | return hashes; 266 | } 267 | } 268 | } 269 | --------------------------------------------------------------------------------