Skip to content

Commit

Permalink
Lookup SRV record, properly do handshake and properly parse ServerStatus
Browse files Browse the repository at this point in the history
  • Loading branch information
psu-de committed Jan 10, 2024
1 parent 463ef30 commit b2c7acc
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 19 deletions.
49 changes: 41 additions & 8 deletions Components/MineSharp.Protocol/IPHelper.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
using DnsClient;
using DnsClient.Protocol;
using MineSharp.Auth.Exceptions;
using MineSharp.Protocol.Exceptions;
using System.Net;

namespace MineSharp.Protocol;

internal static class IPHelper
{
public static IPAddress ResolveHostname(string hostnameOrIp)
private static readonly LookupClient Client = new LookupClient();

public static IPAddress ResolveHostname(string hostnameOrIp, ref ushort port)
{
var type = Uri.CheckHostName(hostnameOrIp);
string ip = type switch {
UriHostNameType.Dns =>
(Dns.GetHostEntry(hostnameOrIp).AddressList.FirstOrDefault()
?? throw new MineSharpHostException($"Could not find ip for hostname ('{hostnameOrIp}')")).ToString(),

UriHostNameType.IPv4 => hostnameOrIp,
return type switch {
UriHostNameType.Dns => _ResolveHostname(hostnameOrIp, ref port),
UriHostNameType.IPv4 => IPAddress.Parse(hostnameOrIp),

_ => throw new MineSharpHostException("Hostname not supported: " + hostnameOrIp)
};
}

private static IPAddress _ResolveHostname(string hostname, ref ushort port)
{
if (port != 25565 || hostname == "localhost")
return DnsLookup(hostname);

var result = Client.Query($"_minecraft._tcp.{hostname}", QueryType.SRV);

if (result.HasError)
return DnsLookup(hostname);

var srvRecord = result.Answers
.OfType<SrvRecord>()
.FirstOrDefault();

return IPAddress.Parse(ip);
if (srvRecord == null)
return DnsLookup(hostname); // No SRV record, fallback to hostname

var serviceName = srvRecord.Target.Value;
if (serviceName.EndsWith('.'))
serviceName = serviceName.Substring(0, serviceName.Length - 1);

var ip = DnsLookup(serviceName);
port = srvRecord.Port;

return ip;
}

private static IPAddress DnsLookup(string hostname)
{
return Client.GetHostEntry(hostname).AddressList.FirstOrDefault()
?? throw new MineSharpAuthException($"Could not find ip for hostname ('{hostname}')");
}
}
1 change: 1 addition & 0 deletions Components/MineSharp.Protocol/MineSharp.Protocol.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.1.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
26 changes: 16 additions & 10 deletions Components/MineSharp.Protocol/MinecraftClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
using MineSharp.Protocol.Packets.Clientbound.Status;
using MineSharp.Protocol.Packets.Handlers;
using MineSharp.Protocol.Packets.Serverbound.Status;
using Newtonsoft.Json;
using NLog;
using System.Diagnostics;
using System.Net.Sockets;
using System.Net;
using MineSharp.Protocol.Packets.Serverbound.Configuration;
using Newtonsoft.Json.Linq;

namespace MineSharp.Protocol;

Expand Down Expand Up @@ -76,6 +76,11 @@ public sealed class MinecraftClient : IDisposable
/// </summary>
public Session Session { get; }

/// <summary>
/// The Hostname of the minecraft server provided in the constructor
/// </summary>
public string Hostname { get; }

/// <summary>
/// The IP Address of the minecraft server
/// </summary>
Expand All @@ -98,7 +103,7 @@ public sealed class MinecraftClient : IDisposable
/// <param name="session">The session object</param>
/// <param name="hostnameOrIp">Hostname or ip of the server</param>
/// <param name="port">Port of the server</param>
public MinecraftClient(MinecraftData data, Session session, string hostnameOrIp, ushort port)
public MinecraftClient(MinecraftData data, Session session, string hostnameOrIp, ushort port = 25565)
{
this._data = data;
this._client = new TcpClient();
Expand All @@ -112,8 +117,9 @@ public MinecraftClient(MinecraftData data, Session session, string hostnameOrIp,
this._useAnonymousNbt = this._data.Version.Protocol >= ProtocolVersion.V_1_20_2;

this.Session = session;
this.IP = IPHelper.ResolveHostname(hostnameOrIp);
this.Port = port;
this.IP = IPHelper.ResolveHostname(hostnameOrIp, ref port);
this.Hostname = hostnameOrIp;
this.GameState = GameState.Handshaking;
}

Expand Down Expand Up @@ -450,9 +456,9 @@ private record PacketSendTask(IPacket Packet, CancellationToken? Token, TaskComp
/// Works only when <see cref="GameState"/> is <see cref="Core.Common.Protocol.GameState.Status"/>.
/// </summary>
/// <returns></returns>
public static async Task<ServerStatusResponseBlob> RequestServerStatus(
public static async Task<ServerStatus> RequestServerStatus(
string hostnameOrIp,
ushort port,
ushort port = 25565,
int timeout = 10000)
{
var latest = MinecraftData.FromVersion(LATEST_SUPPORTED_VERSION);
Expand All @@ -466,12 +472,12 @@ public static async Task<ServerStatusResponseBlob> RequestServerStatus(
throw new MineSharpHostException("Could not connect to server.");

var timeoutCancellation = new CancellationTokenSource();
var taskCompletionSource = new TaskCompletionSource<ServerStatusResponseBlob>();
var taskCompletionSource = new TaskCompletionSource<ServerStatus>();

client.On<StatusResponsePacket>(async packet =>
{
var json = packet.Response;
var response = JsonConvert.DeserializeObject<ServerStatusResponseBlob>(json)!;
var response = ServerStatus.FromJToken(JToken.Parse(json), client._data);
taskCompletionSource.TrySetResult(response);

// the server closes the connection
Expand All @@ -498,10 +504,10 @@ public static async Task<ServerStatusResponseBlob> RequestServerStatus(
/// <param name="hostname"></param>
/// <param name="port"></param>
/// <returns></returns>
public static async Task<MinecraftData> AutodetectServerVersion(string hostname, ushort port)
public static Task<MinecraftData> AutodetectServerVersion(string hostname, ushort port)
{
return await RequestServerStatus(hostname, port)
return RequestServerStatus(hostname, port)
.ContinueWith(
prev => MinecraftData.FromVersion(prev.Result.Version.Name));
prev => MinecraftData.FromVersion(prev.Result.Version));
}
}
2 changes: 1 addition & 1 deletion Components/MineSharp.Protocol/Packets/HandshakeProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static async Task PerformHandshake(MinecraftClient client, GameState next
throw new ArgumentException("Next state must either be Login or Status.");
}

var handshake = new HandshakePacket(data.Version.Protocol, client.IP.ToString(), client.Port, next);
var handshake = new HandshakePacket(data.Version.Protocol, client.Hostname, client.Port, next);
await client.SendPacket(handshake);

if (next == GameState.Status)
Expand Down
144 changes: 144 additions & 0 deletions Components/MineSharp.Protocol/Packets/ServerStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using MineSharp.ChatComponent;
using MineSharp.Core.Common;
using MineSharp.Data;
using Newtonsoft.Json.Linq;
using System.Text.RegularExpressions;

namespace MineSharp.Protocol.Packets;

/// <summary>
/// Represents the status of a server
/// </summary>
public partial class ServerStatus
{
/// <summary>
/// The version string (fe. 1.20.1). Some server's send version ranges, in this case, the latest version is used.
/// </summary>
public readonly string Version;

/// <summary>
/// The protocol version used by the server.
/// </summary>
public readonly int ProtocolVersion;

/// <summary>
/// The server brand. 'Vanilla' if the server does not further specifies it.
/// </summary>
public readonly string Brand;

/// <summary>
/// The max number of players that can join.
/// </summary>
public readonly int MaxPlayers;

/// <summary>
/// How many players are currently on the server.
/// </summary>
public readonly int Online;

/// <summary>
/// A sample of players currently playing.
/// </summary>
public readonly string[] PlayerSample;

/// <summary>
/// The servers MOTD.
/// </summary>
public string MOTD;

/// <summary>
/// The servers favicon as a png data uri.
/// </summary>
public string FavIcon;

/// <summary>
/// Whether the server enforces secure chat.
/// </summary>
public bool EnforceSecureChat;

/// <summary>
///
/// </summary>
public bool PreviewsChat;

private ServerStatus(string version, int protocolVersion, string brand, int maxPlayers, int online, string[] playerSample, string motd, string favIcon, bool enforceSecureChat, bool previewsChat)
{
this.Version = version;
this.ProtocolVersion = protocolVersion;
this.Brand = brand;
this.MaxPlayers = maxPlayers;
this.Online = online;
this.PlayerSample = playerSample;
this.MOTD = motd;
this.FavIcon = favIcon;
this.EnforceSecureChat = enforceSecureChat;
this.PreviewsChat = previewsChat;
}

internal static ServerStatus FromJToken(JToken token, MinecraftData data)
{
var versionToken = token.SelectToken("version") ?? throw new InvalidOperationException();
var playersToken = token.SelectToken("players") ?? throw new InvalidOperationException();

var versionString = (string)versionToken.SelectToken("name")!;
var protocol = (int)versionToken.SelectToken("protocol")!;

var maxPlayers = (int)playersToken.SelectToken("max")!;
var onlinePlayers = (int)playersToken.SelectToken("online")!;

var sampleToken = (JArray)playersToken.SelectToken("sample")!;
var sample = sampleToken.Count > 0 ? sampleToken.Select(x => (string)x.SelectToken("name")!).ToArray() : Array.Empty<string>();

var description = new Chat(token.SelectToken("description")!.ToString(), data).Message;
var favIcon = (string)token.SelectToken("favicon")!;

var enforceSecureChatToken = token.SelectToken("enforcesSecureChat");
var enforceSecureChat = enforceSecureChatToken != null && (bool)enforceSecureChatToken;

var previewsChatToken = token.SelectToken("previewsChat");
var previewsChat = previewsChatToken != null && (bool)previewsChatToken;

(var brand, var version) = ParseVersion(versionString);

return new ServerStatus(
version,
protocol,
brand,
maxPlayers,
onlinePlayers,
sample,
description,
favIcon,
enforceSecureChat,
previewsChat);
}

private static (string Brand, string Version) ParseVersion(string versionString)
{
var match = ParseVersionString().Match(versionString);

var brand = match.Groups[1].Value.TrimEnd(' ');
var version = match.Groups[2].Value;

if (string.IsNullOrEmpty(brand))
brand = "Vanilla";

if (version.EndsWith('x'))
version = version.Replace('x', '1');

return (brand, version);
}

/// <inheritdoc />
public override string ToString() => $"ServerStatus (Brand={Brand}, " +
$"Version={this.Version}, " +
$"Protocol={this.ProtocolVersion}, " +
$"MaxPlayers={this.MaxPlayers}, " +
$"Online={this.Online}, " +
$"MOTD={this.MOTD}, " +
$"EnforcesSecureChat={this.EnforceSecureChat}, " +
$"PreviewsChat={this.PreviewsChat})";

[GeneratedRegex(@"^([a-zA-Z_ ]*)(1\.\d\d?(?:\.(?:\d\d?|x))?-?)*")]
private static partial Regex ParseVersionString();
}

0 comments on commit b2c7acc

Please sign in to comment.