Refactoring to asynchronous. (partially completed)

This commit is contained in:
BruceChen 2022-12-20 22:41:14 +08:00
parent 7ee08092d4
commit 096ea0c70c
72 changed files with 6033 additions and 5080 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MinecraftClient.Protocol.Handlers.Forge
{
@ -11,16 +12,22 @@ namespace MinecraftClient.Protocol.Handlers.Forge
/// <summary>
/// Represents an individual forge mod.
/// </summary>
public class ForgeMod
public record ForgeMod
{
public ForgeMod(String ModID, String Version)
public ForgeMod(string? modID, string? version)
{
this.ModID = ModID;
this.Version = Version;
ModID = modID;
Version = ModMarker = version;
}
public readonly String ModID;
public readonly String Version;
[JsonPropertyName("modId")]
public string? ModID { init; get; }
[JsonPropertyName("version")]
public string? Version { init; get; }
[JsonPropertyName("modmarker")]
public string? ModMarker { init; get; }
public override string ToString()
{
@ -138,5 +145,16 @@ namespace MinecraftClient.Protocol.Handlers.Forge
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
}
/// <summary>
/// Create a new ForgeInfo from the given data.
/// </summary>
/// <param name="data">The modinfo JSON tag.</param>
/// <param name="fmlVersion">Forge protocol version</param>
internal ForgeInfo(ForgeMod[] mods, FMLVersion fmlVersion)
{
Mods = new(mods);
Version = fmlVersion;
}
}
}

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MinecraftClient.Protocol.PacketPipeline;
namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
@ -8,7 +10,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private static int RootIdx;
private static CommandNode[] Nodes = Array.Empty<CommandNode>();
public static void Read(DataTypes dataTypes, Queue<byte> packetData)
public static async Task Read(DataTypes dataTypes, PacketStream packetData)
{
int count = dataTypes.ReadNextVarInt(packetData);
Nodes = new CommandNode[count];
@ -23,7 +25,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
int redirectNode = ((flags & 0x08) > 0) ? dataTypes.ReadNextVarInt(packetData) : -1;
string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? dataTypes.ReadNextString(packetData) : null;
string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? (await dataTypes.ReadNextStringAsync(packetData)) : null;
int paserId = ((flags & 0x03) == 2) ? dataTypes.ReadNextVarInt(packetData) : -1;
Paser? paser = null;
@ -50,7 +52,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
};
}
string? suggestionsType = ((flags & 0x10) > 0) ? dataTypes.ReadNextString(packetData) : null;
string? suggestionsType = ((flags & 0x10) > 0) ? (await dataTypes.ReadNextStringAsync(packetData)) : null;
Nodes[i] = new(flags, childs, redirectNode, name, paser, suggestionsType);
}
@ -158,7 +160,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserEmpty : Paser
{
public PaserEmpty(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserEmpty(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -181,7 +183,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private float Min = float.MinValue, Max = float.MaxValue;
public PaserFloat(DataTypes dataTypes, Queue<byte> packetData)
public PaserFloat(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -211,7 +213,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private double Min = double.MinValue, Max = double.MaxValue;
public PaserDouble(DataTypes dataTypes, Queue<byte> packetData)
public PaserDouble(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -241,7 +243,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private int Min = int.MinValue, Max = int.MaxValue;
public PaserInteger(DataTypes dataTypes, Queue<byte> packetData)
public PaserInteger(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -271,7 +273,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private long Min = long.MinValue, Max = long.MaxValue;
public PaserLong(DataTypes dataTypes, Queue<byte> packetData)
public PaserLong(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -302,7 +304,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private enum StringType { SINGLE_WORD, QUOTABLE_PHRASE, GREEDY_PHRASE };
public PaserString(DataTypes dataTypes, Queue<byte> packetData)
public PaserString(DataTypes dataTypes, PacketStream packetData)
{
Type = (StringType)dataTypes.ReadNextVarInt(packetData);
}
@ -327,7 +329,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private byte Flags;
public PaserEntity(DataTypes dataTypes, Queue<byte> packetData)
public PaserEntity(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
}
@ -351,7 +353,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserBlockPos : Paser
{
public PaserBlockPos(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserBlockPos(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -372,7 +374,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserColumnPos : Paser
{
public PaserColumnPos(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserColumnPos(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -393,7 +395,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserVec3 : Paser
{
public PaserVec3(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserVec3(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -414,7 +416,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserVec2 : Paser
{
public PaserVec2(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserVec2(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -435,7 +437,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserRotation : Paser
{
public PaserRotation(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserRotation(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -455,7 +457,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserMessage : Paser
{
public PaserMessage(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserMessage(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -477,7 +479,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private byte Flags;
public PaserScoreHolder(DataTypes dataTypes, Queue<byte> packetData)
public PaserScoreHolder(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
}
@ -502,7 +504,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private bool Decimals;
public PaserRange(DataTypes dataTypes, Queue<byte> packetData)
public PaserRange(DataTypes dataTypes, PacketStream packetData)
{
Decimals = dataTypes.ReadNextBool(packetData);
}
@ -527,9 +529,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private string Registry;
public PaserResourceOrTag(DataTypes dataTypes, Queue<byte> packetData)
public PaserResourceOrTag(DataTypes dataTypes, PacketStream packetData)
{
Registry = dataTypes.ReadNextString(packetData);
var task = dataTypes.ReadNextStringAsync(packetData);
task.Wait();
Registry = task.Result;
}
public override bool Check(string text)
@ -552,9 +556,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private string Registry;
public PaserResource(DataTypes dataTypes, Queue<byte> packetData)
public PaserResource(DataTypes dataTypes, PacketStream packetData)
{
Registry = dataTypes.ReadNextString(packetData);
var task = dataTypes.ReadNextStringAsync(packetData);
task.Wait();
Registry = task.Result;
}
public override bool Check(string text)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,9 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Handlers.Forge;
using MinecraftClient.Protocol.Message;
using MinecraftClient.Protocol.PacketPipeline;
using MinecraftClient.Scripting;
using static MinecraftClient.Protocol.Handlers.Protocol18Handler;
namespace MinecraftClient.Protocol.Handlers
{
@ -54,23 +57,23 @@ namespace MinecraftClient.Protocol.Handlers
/// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML)
/// </summary>
/// <returns>Whether the handshake was successful.</returns>
public bool CompleteForgeHandshake()
public async Task<bool> CompleteForgeHandshake(SocketWrapper socketWrapper)
{
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML)
{
while (fmlHandshakeState != FMLHandshakeClientState.DONE)
{
(int packetID, Queue<byte> packetData) = protocol18.ReadNextPacket();
(int packetID, PacketStream packetStream) = await socketWrapper.GetNextPacket(handleCompress: true);
if (packetID == 0x40) // Disconnect
{
mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData)));
mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetStream)));
return false;
}
else
{
// Send back regular packet to the vanilla protocol handler
protocol18.HandlePacket(packetID, packetData);
await protocol18.HandlePacket(packetID, packetStream);
}
}
}
@ -82,7 +85,7 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
/// <param name="packetData">Packet data to read from</param>
/// <returns>Length from packet data</returns>
public int ReadNextVarShort(Queue<byte> packetData)
public int ReadNextVarShort(PacketStream packetData)
{
if (ForgeEnabled())
{
@ -103,10 +106,11 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="packetData">Plugin message data</param>
/// <param name="currentDimension">Current world dimension</param>
/// <returns>TRUE if the plugin message was recognized and handled</returns>
public bool HandlePluginMessage(string channel, Queue<byte> packetData, ref int currentDimension)
public async Task<Tuple<bool, int>> HandlePluginMessage(string channel, byte[] packetDataArr, int currentDimension)
{
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE)
{
Queue<byte> packetData = new(packetDataArr);
if (channel == "FML|HS")
{
FMLHandshakeDiscriminator discriminator = (FMLHandshakeDiscriminator)dataTypes.ReadNextByte(packetData);
@ -114,21 +118,21 @@ namespace MinecraftClient.Protocol.Handlers
if (discriminator == FMLHandshakeDiscriminator.HandshakeReset)
{
fmlHandshakeState = FMLHandshakeClientState.START;
return true;
return new(true, currentDimension);
}
switch (fmlHandshakeState)
{
case FMLHandshakeClientState.START:
if (discriminator != FMLHandshakeDiscriminator.ServerHello)
return false;
return new(false, currentDimension);
// Send the plugin channel registration.
// REGISTER is somewhat special in that it doesn't actually include length information,
// and is also \0-separated.
// Also, yes, "FML" is there twice. Don't ask me why, but that's the way forge does it.
string[] channels = { "FML|HS", "FML", "FML|MP", "FML", "FORGE" };
protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels)));
await protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels)));
byte fmlProtocolVersion = dataTypes.ReadNextByte(packetData);
@ -139,7 +143,7 @@ namespace MinecraftClient.Protocol.Handlers
currentDimension = dataTypes.ReadNextInt(packetData);
// Tell the server we're running the same version.
SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion });
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion });
// Then tell the server that we're running the same mods.
if (Settings.Config.Logging.DebugMessages)
@ -148,17 +152,17 @@ namespace MinecraftClient.Protocol.Handlers
for (int i = 0; i < forgeInfo.Mods.Count; i++)
{
ForgeInfo.ForgeMod mod = forgeInfo.Mods[i];
mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID), dataTypes.GetString(mod.Version));
mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID!), dataTypes.GetString(mod.Version ?? mod.ModMarker ?? string.Empty));
}
SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList,
dataTypes.ConcatBytes(dataTypes.GetVarInt(forgeInfo.Mods.Count), dataTypes.ConcatBytes(mods)));
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERDATA;
return true;
return new(true, currentDimension);
case FMLHandshakeClientState.WAITINGSERVERDATA:
if (discriminator != FMLHandshakeDiscriminator.ModList)
return false;
return new(false, currentDimension);
Thread.Sleep(2000);
@ -167,16 +171,16 @@ namespace MinecraftClient.Protocol.Handlers
// Tell the server that yes, we are OK with the mods it has
// even though we don't actually care what mods it has.
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.WAITINGSERVERDATA });
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERCOMPLETE;
return false;
return new(false, currentDimension);
case FMLHandshakeClientState.WAITINGSERVERCOMPLETE:
// The server now will tell us a bunch of registry information.
// We need to read it all, though, until it says that there is no more.
if (discriminator != FMLHandshakeDiscriminator.RegistryData)
return false;
return new(false, currentDimension);
if (protocolversion < Protocol18Handler.MC_1_8_Version)
{
@ -202,34 +206,34 @@ namespace MinecraftClient.Protocol.Handlers
fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE;
}
return false;
return new(false, currentDimension);
case FMLHandshakeClientState.PENDINGCOMPLETE:
// The server will ask us to accept the registries.
// Just say yes.
if (discriminator != FMLHandshakeDiscriminator.HandshakeAck)
return false;
return new(false, currentDimension);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_accept_registry, acceptnewlines: true);
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.PENDINGCOMPLETE });
fmlHandshakeState = FMLHandshakeClientState.COMPLETE;
return true;
return new(true, currentDimension);
case FMLHandshakeClientState.COMPLETE:
// One final "OK". On the actual forge source, a packet is sent from
// the client to the client saying that the connection was complete, but
// we don't need to do that.
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.COMPLETE });
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLine(Translations.forge_complete);
fmlHandshakeState = FMLHandshakeClientState.DONE;
return true;
return new(true, currentDimension);
}
}
}
return false;
return new(false, currentDimension);
}
/// <summary>
@ -239,8 +243,9 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="packetData">Plugin message data</param>
/// <param name="responseData">Response data to return to server</param>
/// <returns>TRUE/FALSE depending on whether the packet was understood or not</returns>
public bool HandleLoginPluginRequest(string channel, Queue<byte> packetData, ref List<byte> responseData)
public async Task<Tuple<bool, List<byte>>> HandleLoginPluginRequest(string channel, PacketStream packetData)
{
List<byte> responseData = new();
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML2 && channel == "fml:loginwrapper")
{
// Forge Handshake handler source code used to implement the FML2 packets:
@ -278,8 +283,8 @@ namespace MinecraftClient.Protocol.Handlers
// The content of each message is mapped into a class inside FMLHandshakeMessages.java
// FMLHandshakeHandler will then process the packet, e.g. handleServerModListOnClient() for Server Mod List.
string fmlChannel = dataTypes.ReadNextString(packetData);
dataTypes.ReadNextVarInt(packetData); // Packet length
string fmlChannel = await dataTypes.ReadNextStringAsync(packetData);
dataTypes.SkipNextVarInt(packetData); // Packet length
int packetID = dataTypes.ReadNextVarInt(packetData);
if (fmlChannel == "fml:handshake")
@ -308,17 +313,17 @@ namespace MinecraftClient.Protocol.Handlers
List<string> mods = new();
int modCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < modCount; i++)
mods.Add(dataTypes.ReadNextString(packetData));
mods.Add(await dataTypes.ReadNextStringAsync(packetData));
Dictionary<string, string> channels = new();
int channelCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < channelCount; i++)
channels.Add(dataTypes.ReadNextString(packetData), dataTypes.ReadNextString(packetData));
channels.Add(await dataTypes.ReadNextStringAsync(packetData), await dataTypes.ReadNextStringAsync(packetData));
List<string> registries = new();
int registryCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < registryCount; i++)
registries.Add(dataTypes.ReadNextString(packetData));
registries.Add(await dataTypes.ReadNextStringAsync(packetData));
// Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode()
//
@ -372,7 +377,7 @@ namespace MinecraftClient.Protocol.Handlers
if (Settings.Config.Logging.DebugMessages)
{
string registryName = dataTypes.ReadNextString(packetData);
string registryName = await dataTypes.ReadNextStringAsync(packetData);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_registry, registryName));
}
@ -391,7 +396,7 @@ namespace MinecraftClient.Protocol.Handlers
if (Settings.Config.Logging.DebugMessages)
{
string configName = dataTypes.ReadNextString(packetData);
string configName = await dataTypes.ReadNextStringAsync(packetData);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_config, configName));
}
@ -408,11 +413,10 @@ namespace MinecraftClient.Protocol.Handlers
if (fmlResponseReady)
{
// Wrap our FML packet into a LoginPluginResponse payload
responseData.Clear();
responseData.AddRange(dataTypes.GetString(fmlChannel));
responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count));
responseData.AddRange(fmlResponsePacket);
return true;
return new(true, responseData);
}
}
else if (Settings.Config.Logging.DebugMessages)
@ -420,7 +424,7 @@ namespace MinecraftClient.Protocol.Handlers
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_unknown_channel, fmlChannel));
}
}
return false;
return new(false, responseData);
}
/// <summary>
@ -428,9 +432,9 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
/// <param name="discriminator">Discriminator to use.</param>
/// <param name="data">packet Data</param>
private void SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data)
private async Task SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data)
{
protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data));
await protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data));
}
/// <summary>
@ -439,10 +443,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns>
public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo? forgeInfo)
public static bool ServerInfoCheckForge(PingResult jsonData, ref ForgeInfo? forgeInfo)
{
return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater
return ServerInfoCheckForgeSubFML1(jsonData, ref forgeInfo) // MC 1.12 and lower
|| ServerInfoCheckForgeSubFML2(jsonData, ref forgeInfo); // MC 1.13 and greater
}
/// <summary>
@ -474,38 +478,21 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <param name="fmlVersion">Forge protocol version</param>
/// <returns>True if the server is running Forge</returns>
private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion)
private static bool ServerInfoCheckForgeSubFML1(PingResult jsonData, ref ForgeInfo? forgeInfo)
{
string forgeDataTag;
string versionField;
string versionString;
switch (fmlVersion)
if (jsonData.modinfo != null)
{
case FMLVersion.FML:
forgeDataTag = "modinfo";
versionField = "type";
versionString = "FML";
break;
case FMLVersion.FML2:
forgeDataTag = "forgeData";
versionField = "fmlNetworkVersion";
versionString = "2";
break;
default:
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object)
{
Json.JSONData modData = jsonData.Properties[forgeDataTag];
if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString)
if (jsonData.modinfo.type == "FML")
{
forgeInfo = new ForgeInfo(modData, fmlVersion);
if (forgeInfo.Mods.Any())
if (jsonData.modinfo.modList == null || jsonData.modinfo.modList.Length == 0)
{
forgeInfo = null;
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
}
else
{
forgeInfo = new ForgeInfo(jsonData.modinfo.modList, FMLVersion.FML);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count));
if (Settings.Config.Logging.DebugMessages)
{
@ -515,10 +502,39 @@ namespace MinecraftClient.Protocol.Handlers
}
return true;
}
}
}
return false;
}
/// <summary>
/// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2
/// </summary>
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns>
private static bool ServerInfoCheckForgeSubFML2(PingResult jsonData, ref ForgeInfo? forgeInfo)
{
if (jsonData.forgeData != null)
{
if (jsonData.forgeData.fmlNetworkVersion == "2")
{
if (jsonData.forgeData.mods == null || jsonData.forgeData.mods.Length == 0)
{
forgeInfo = null;
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
}
else
{
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
forgeInfo = null;
forgeInfo = new ForgeInfo(jsonData.forgeData.mods, FMLVersion.FML2);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count));
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_mod_list, acceptnewlines: true);
foreach (ForgeInfo.ForgeMod mod in forgeInfo.Mods)
ConsoleIO.WriteLineFormatted("§8 " + mod.ToString());
}
return true;
}
}
}

View file

@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
//using System.Linq;
//using System.Text;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol.PacketPipeline;
namespace MinecraftClient.Protocol.Handlers
{
@ -33,21 +35,21 @@ namespace MinecraftClient.Protocol.Handlers
/// <summary>
/// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section.
/// </summary>
/// <param name="cache">Cache for reading data</param>
/// <param name="stream">Cache for reading data</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private Chunk? ReadBlockStatesField(Queue<byte> cache)
private async Task<Chunk?> ReadBlockStatesFieldAsync(PacketStream stream)
{
// read Block states (Type: Paletted Container)
byte bitsPerEntry = dataTypes.ReadNextByte(cache);
byte bitsPerEntry = await dataTypes.ReadNextByteAsync(stream);
// 1.18(1.18.1) add a pattle named "Single valued" to replace the vertical strip bitmask in the old
if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC_1_18_1_Version)
{
// Palettes: Single valued - 1.18(1.18.1) and above
ushort blockId = (ushort)dataTypes.ReadNextVarInt(cache);
ushort blockId = (ushort)(await dataTypes.ReadNextVarIntAsync(stream));
Block block = new(blockId);
dataTypes.SkipNextVarInt(cache); // Data Array Length will be zero
dataTypes.SkipNextVarInt(stream); // Data Array Length will be zero
// Empty chunks will not be stored
if (block.Type == Material.Air)
@ -73,16 +75,16 @@ namespace MinecraftClient.Protocol.Handlers
// EG, if bitsPerEntry = 5, valueMask = 00011111 in binary
uint valueMask = (uint)((1 << bitsPerEntry) - 1);
int paletteLength = usePalette ? dataTypes.ReadNextVarInt(cache) : 0; // Assume zero when length is absent
int paletteLength = usePalette ? await dataTypes.ReadNextVarIntAsync(stream) : 0; // Assume zero when length is absent
Span<uint> palette = paletteLength < 256 ? stackalloc uint[paletteLength] : new uint[paletteLength];
uint[] palette = new uint[paletteLength];
for (int i = 0; i < paletteLength; i++)
palette[i] = (uint)dataTypes.ReadNextVarInt(cache);
palette[i] = (uint)(await dataTypes.ReadNextVarIntAsync(stream));
//// Block IDs are packed in the array of 64-bits integers
dataTypes.SkipNextVarInt(cache); // Entry length
Span<byte> entryDataByte = stackalloc byte[8];
Span<long> entryDataLong = MemoryMarshal.Cast<byte, long>(entryDataByte); // Faster than MemoryMarshal.Read<long>
dataTypes.SkipNextVarInt(stream); // Entry length
long entryData = 0;
Chunk chunk = new();
int startOffset = 64; // Read the first data immediately
@ -101,10 +103,10 @@ namespace MinecraftClient.Protocol.Handlers
// When overlapping, move forward to the beginning of the next Long
startOffset = 0;
dataTypes.ReadDataReverse(cache, entryDataByte); // read long
entryData = await dataTypes.ReadNextLongAsync(stream);
}
uint blockId = (uint)(entryDataLong[0] >> startOffset) & valueMask;
uint blockId = (uint)(entryData >> startOffset) & valueMask;
// Map small IDs to actual larger block IDs
if (usePalette)
@ -141,10 +143,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="chunkX">Chunk X location</param>
/// <param name="chunkZ">Chunk Z location</param>
/// <param name="verticalStripBitmask">Chunk mask for reading data, store in bitset, used in 1.17 and 1.17.1</param>
/// <param name="cache">Cache for reading chunk data</param>
/// <param name="stream">Cache for reading chunk data</param>
/// <param name="cancellationToken">token to cancel the task</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue<byte> cache)
public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, PacketStream stream)
{
World world = handler.GetWorld();
@ -181,10 +183,10 @@ namespace MinecraftClient.Protocol.Handlers
((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0))
{
// Non-air block count inside chunk section, for lighting purposes
int blockCnt = dataTypes.ReadNextShort(cache);
int blockCnt = await dataTypes.ReadNextShortAsync(stream);
// Read Block states (Type: Paletted Container)
Chunk? chunk = ReadBlockStatesField(cache);
Chunk? chunk = await ReadBlockStatesFieldAsync(stream);
//We have our chunk, save the chunk into the world
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == lastChunkY);
@ -192,23 +194,23 @@ namespace MinecraftClient.Protocol.Handlers
// Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above
if (protocolversion >= Protocol18Handler.MC_1_18_1_Version)
{
byte bitsPerEntryBiome = dataTypes.ReadNextByte(cache); // Bits Per Entry
byte bitsPerEntryBiome = await dataTypes.ReadNextByteAsync(stream); // Bits Per Entry
if (bitsPerEntryBiome == 0)
{
dataTypes.SkipNextVarInt(cache); // Value
dataTypes.SkipNextVarInt(cache); // Data Array Length
dataTypes.SkipNextVarInt(stream); // Value
dataTypes.SkipNextVarInt(stream); // Data Array Length
// Data Array must be empty
}
else
{
if (bitsPerEntryBiome <= 3)
{
int paletteLength = dataTypes.ReadNextVarInt(cache); // Palette Length
int paletteLength = await dataTypes.ReadNextVarIntAsync(stream); // Palette Length
for (int i = 0; i < paletteLength; i++)
dataTypes.SkipNextVarInt(cache); // Palette
dataTypes.SkipNextVarInt(stream); // Palette
}
int dataArrayLength = dataTypes.ReadNextVarInt(cache); // Data Array Length
dataTypes.DropData(dataArrayLength * 8, cache); // Data Array
int dataArrayLength = await dataTypes.ReadNextVarIntAsync(stream); // Data Array Length
await dataTypes.DropDataAsync(dataArrayLength * 8, stream); // Data Array
}
}
}
@ -228,10 +230,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="hasSkyLight">Contains skylight info</param>
/// <param name="chunksContinuous">Are the chunk continuous</param>
/// <param name="currentDimension">Current dimension type (0 = overworld)</param>
/// <param name="cache">Cache for reading chunk data</param>
/// <param name="stream">Cache for reading chunk data</param>
/// <param name="cancellationToken">token to cancel the task</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue<byte> cache)
public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, PacketStream stream)
{
World world = handler.GetWorld();
@ -247,9 +249,9 @@ namespace MinecraftClient.Protocol.Handlers
{
// 1.14 and above Non-air block count inside chunk section, for lighting purposes
if (protocolversion >= Protocol18Handler.MC_1_14_Version)
dataTypes.ReadNextShort(cache);
await dataTypes.SkipNextShortAsync(stream);
byte bitsPerBlock = dataTypes.ReadNextByte(cache);
byte bitsPerBlock = await dataTypes.ReadNextByteAsync(stream);
bool usePalette = (bitsPerBlock <= 8);
// Vanilla Minecraft will use at least 4 bits per block
@ -260,12 +262,12 @@ namespace MinecraftClient.Protocol.Handlers
// is not used, MC 1.13+ does not send the field at all in this case
int paletteLength = 0; // Assume zero when length is absent
if (usePalette || protocolversion < Protocol18Handler.MC_1_13_Version)
paletteLength = dataTypes.ReadNextVarInt(cache);
paletteLength = await dataTypes.ReadNextVarIntAsync(stream);
int[] palette = new int[paletteLength];
for (int i = 0; i < paletteLength; i++)
{
palette[i] = dataTypes.ReadNextVarInt(cache);
palette[i] = await dataTypes.ReadNextVarIntAsync(stream);
}
// Bit mask covering bitsPerBlock bits
@ -273,7 +275,7 @@ namespace MinecraftClient.Protocol.Handlers
uint valueMask = (uint)((1 << bitsPerBlock) - 1);
// Block IDs are packed in the array of 64-bits integers
ulong[] dataArray = dataTypes.ReadNextULongArray(cache);
ulong[] dataArray = await dataTypes.ReadNextULongArrayAsync(stream);
Chunk chunk = new();
@ -358,19 +360,19 @@ namespace MinecraftClient.Protocol.Handlers
}
}
//We have our chunk, save the chunk into the world
// We have our chunk, save the chunk into the world
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY);
//Pre-1.14 Lighting data
// Pre-1.14 Lighting data
if (protocolversion < Protocol18Handler.MC_1_14_Version)
{
//Skip block light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
// Skip block light
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
//Skip sky light
// Skip sky light
if (currentDimension == 0)
// Sky light is not sent in the nether or the end
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
}
}
}
@ -383,15 +385,12 @@ namespace MinecraftClient.Protocol.Handlers
// 1.8 chunk format
if (chunksContinuous && chunkMask == 0)
{
//Unload the entire chunk column
handler.InvokeOnMainThread(() =>
{
world[chunkX, chunkZ] = null;
});
// Unload the entire chunk column
world[chunkX, chunkZ] = null;
}
else
{
//Load chunk data from the server
// Load chunk data from the server
int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++)
{
@ -399,35 +398,34 @@ namespace MinecraftClient.Protocol.Handlers
{
Chunk chunk = new();
//Read chunk data, all at once for performance reasons, and build the chunk object
Queue<ushort> queue = new(dataTypes.ReadNextUShortsLittleEndian(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ, cache));
// Read chunk data, all at once for performance reasons, and build the chunk object
for (int blockY = 0; blockY < Chunk.SizeY; blockY++)
for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++)
for (int blockX = 0; blockX < Chunk.SizeX; blockX++)
chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(queue.Dequeue()));
chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(await dataTypes.ReadNextUShortAsync(stream)));
//We have our chunk, save the chunk into the world
// We have our chunk, save the chunk into the world
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY);
}
}
//Skip light information
// Skip light information
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
{
if ((chunkMask & (1 << chunkY)) != 0)
{
//Skip block light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
// Skip block light
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
//Skip sky light
// Skip sky light
if (hasSkyLight)
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
}
}
//Skip biome metadata
// Skip biome metadata
if (chunksContinuous)
dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache);
await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream);
}
}
else
@ -435,15 +433,12 @@ namespace MinecraftClient.Protocol.Handlers
// 1.7 chunk format
if (chunksContinuous && chunkMask == 0)
{
//Unload the entire chunk column
handler.InvokeOnMainThread(() =>
{
world[chunkX, chunkZ] = null;
});
// Unload the entire chunk column
world[chunkX, chunkZ] = null;
}
else
{
//Count chunk sections
// Count chunk sections
int sectionCount = 0;
int addDataSectionCount = 0;
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
@ -454,10 +449,10 @@ namespace MinecraftClient.Protocol.Handlers
addDataSectionCount++;
}
//Read chunk data, unpacking 4-bit values into 8-bit values for block metadata
Queue<byte> blockTypes = new(dataTypes.ReadData(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, cache));
// Read chunk data, unpacking 4-bit values into 8-bit values for block metadata
Queue<byte> blockTypes = new(await dataTypes.ReadDataAsync(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, stream));
Queue<byte> blockMeta = new();
foreach (byte packed in dataTypes.ReadData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache))
foreach (byte packed in await dataTypes.ReadDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream))
{
byte hig = (byte)(packed >> 4);
byte low = (byte)(packed & (byte)0x0F);
@ -465,15 +460,15 @@ namespace MinecraftClient.Protocol.Handlers
blockMeta.Enqueue(low);
}
//Skip data we don't need
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Block light
// Skip data we don't need
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Block light
if (hasSkyLight)
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Sky light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, cache); //BlockAdd
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Sky light
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, stream); //BlockAdd
if (chunksContinuous)
dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache); //Biomes
await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream); //Biomes
//Load chunk data
// Load chunk data
int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++)
{

View file

@ -1,114 +0,0 @@
using System;
using System.Net.Sockets;
using MinecraftClient.Crypto;
namespace MinecraftClient.Protocol.Handlers
{
/// <summary>
/// Wrapper for handling unencrypted & encrypted socket
/// </summary>
class SocketWrapper
{
readonly TcpClient c;
AesCfb8Stream? s;
bool encrypted = false;
/// <summary>
/// Initialize a new SocketWrapper
/// </summary>
/// <param name="client">TcpClient connected to the server</param>
public SocketWrapper(TcpClient client)
{
c = client;
}
/// <summary>
/// Check if the socket is still connected
/// </summary>
/// <returns>TRUE if still connected</returns>
/// <remarks>Silently dropped connection can only be detected by attempting to read/write data</remarks>
public bool IsConnected()
{
return c.Client != null && c.Connected;
}
/// <summary>
/// Check if the socket has data available to read
/// </summary>
/// <returns>TRUE if data is available to read</returns>
public bool HasDataAvailable()
{
return c.Client.Available > 0;
}
/// <summary>
/// Switch network reading/writing to an encrypted stream
/// </summary>
/// <param name="secretKey">AES secret key</param>
public void SwitchToEncrypted(byte[] secretKey)
{
if (encrypted)
throw new InvalidOperationException("Stream is already encrypted!?");
s = new AesCfb8Stream(c.GetStream(), secretKey);
encrypted = true;
}
/// <summary>
/// Network reading method. Read bytes from the socket or encrypted socket.
/// </summary>
private void Receive(byte[] buffer, int start, int offset, SocketFlags f)
{
int read = 0;
while (read < offset)
{
if (encrypted)
read += s!.Read(buffer, start + read, offset - read);
else
read += c.Client.Receive(buffer, start + read, offset - read, f);
}
}
/// <summary>
/// Read some data from the server.
/// </summary>
/// <param name="length">Amount of bytes to read</param>
/// <returns>The data read from the network as an array</returns>
public byte[] ReadDataRAW(int length)
{
if (length > 0)
{
byte[] cache = new byte[length];
Receive(cache, 0, length, SocketFlags.None);
return cache;
}
return Array.Empty<byte>();
}
/// <summary>
/// Send raw data to the server.
/// </summary>
/// <param name="buffer">data to send</param>
public void SendDataRAW(byte[] buffer)
{
if (encrypted)
s!.Write(buffer, 0, buffer.Length);
else
c.Client.Send(buffer);
}
/// <summary>
/// Disconnect from the server
/// </summary>
public void Disconnect()
{
try
{
c.Close();
}
catch (SocketException) { }
catch (System.IO.IOException) { }
catch (NullReferenceException) { }
catch (ObjectDisposedException) { }
}
}
}

View file

@ -1,63 +0,0 @@
using Ionic.Zlib;
namespace MinecraftClient.Protocol.Handlers
{
/// <summary>
/// Quick Zlib compression handling for network packet compression.
/// Note: Underlying compression handling is taken from the DotNetZip Library.
/// This library is open source and provided under the Microsoft Public License.
/// More info about DotNetZip at dotnetzip.codeplex.com.
/// </summary>
public static class ZlibUtils
{
/// <summary>
/// Compress a byte array into another bytes array using Zlib compression
/// </summary>
/// <param name="to_compress">Data to compress</param>
/// <returns>Compressed data as a byte array</returns>
public static byte[] Compress(byte[] to_compress)
{
byte[] data;
using (System.IO.MemoryStream memstream = new())
{
using (ZlibStream stream = new(memstream, CompressionMode.Compress))
{
stream.Write(to_compress, 0, to_compress.Length);
}
data = memstream.ToArray();
}
return data;
}
/// <summary>
/// Decompress a byte array into another byte array of the specified size
/// </summary>
/// <param name="to_decompress">Data to decompress</param>
/// <param name="size_uncompressed">Size of the data once decompressed</param>
/// <returns>Decompressed data as a byte array</returns>
public static byte[] Decompress(byte[] to_decompress, int size_uncompressed)
{
ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress);
byte[] packetData_decompressed = new byte[size_uncompressed];
stream.Read(packetData_decompressed, 0, size_uncompressed);
stream.Close();
return packetData_decompressed;
}
/// <summary>
/// Decompress a byte array into another byte array of a potentially unlimited size (!)
/// </summary>
/// <param name="to_decompress">Data to decompress</param>
/// <returns>Decompressed data as byte array</returns>
public static byte[] Decompress(byte[] to_decompress)
{
ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress);
byte[] buffer = new byte[16 * 1024];
using System.IO.MemoryStream decompressedBuffer = new();
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
decompressedBuffer.Write(buffer, 0, read);
return decompressedBuffer.ToArray();
}
}
}

View file

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Inventory;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol.ProfileKey;
@ -19,7 +22,12 @@ namespace MinecraftClient.Protocol
/// Start the login procedure once connected to the server
/// </summary>
/// <returns>True if login was successful</returns>
bool Login(PlayerKeyPair? playerKeyPair, Session.SessionToken session);
Task<bool> Login(HttpClient httpClient, PlayerKeyPair? playerKeyPair, Session.SessionToken session);
/// <summary>
/// Start processing game packets.
/// </summary>
Task StartUpdating();
/// <summary>
/// Disconnect from the server
@ -46,20 +54,20 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="message">Text to send</param>
/// <returns>True if successfully sent</returns>
bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
Task<bool> SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
/// <summary>
/// Allow to respawn after death
/// </summary>
/// <returns>True if packet successfully sent</returns>
bool SendRespawnPacket();
Task<bool> SendRespawnPacket();
/// <summary>
/// Inform the server of the client being used to connect
/// </summary>
/// <param name="brandInfo">Client string describing the client</param>
/// <returns>True if brand info was successfully sent</returns>
bool SendBrandInfo(string brandInfo);
Task<bool> SendBrandInfo(string brandInfo);
/// <summary>
/// Inform the server of the client's Minecraft settings
@ -72,7 +80,7 @@ namespace MinecraftClient.Protocol
/// <param name="skinParts">Show skin layers</param>
/// <param name="mainHand">1.9+ main hand</param>
/// <returns>True if client settings were successfully sent</returns>
bool SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand);
Task<bool> SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand);
/// <summary>
/// Send a location update telling that we moved to that location
@ -82,7 +90,7 @@ namespace MinecraftClient.Protocol
/// <param name="yaw">The new yaw (optional)</param>
/// <param name="pitch">The new pitch (optional)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch);
Task<bool> SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch);
/// <summary>
/// Send a plugin channel packet to the server.
@ -91,7 +99,7 @@ namespace MinecraftClient.Protocol
/// <param name="channel">Channel to send packet on</param>
/// <param name="data">packet Data</param>
/// <returns>True if message was successfully sent</returns>
bool SendPluginChannelPacket(string channel, byte[] data);
Task<bool> SendPluginChannelPacket(string channel, byte[] data);
/// <summary>
/// Send Entity Action packet to the server.
@ -99,14 +107,14 @@ namespace MinecraftClient.Protocol
/// <param name="entityID">PlayerID</param>
/// <param name="type">Type of packet to send</param>
/// <returns>True if packet was successfully sent</returns>
bool SendEntityAction(int EntityID, int type);
Task<bool> SendEntityAction(int EntityID, int type);
/// <summary>
/// Send a held item change packet to the server.
/// </summary>
/// <param name="slot">New active slot in the inventory hotbar</param>
/// <returns>True if packet was successfully sent</returns>
bool SendHeldItemChange(short slot);
Task<bool> SendHeldItemChange(short slot);
/// <summary>
/// Send an entity interaction packet to the server.
@ -114,7 +122,7 @@ namespace MinecraftClient.Protocol
/// <param name="EntityID">Entity ID to interact with</param>
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type);
Task<bool> SendInteractEntity(int EntityID, int type);
/// <summary>
/// Send an entity interaction packet to the server.
@ -126,7 +134,7 @@ namespace MinecraftClient.Protocol
/// <param name="Z">Z coordinate for "interact at"</param>
/// <param name="hand">Player hand (0: main hand, 1: off hand)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand);
Task<bool> SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand);
/// <summary>
/// Send an entity interaction packet to the server.
@ -137,7 +145,7 @@ namespace MinecraftClient.Protocol
/// <param name="Y">Y coordinate for "interact at"</param>
/// <param name="Z">Z coordinate for "interact at"</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z);
Task<bool> SendInteractEntity(int EntityID, int type, float X, float Y, float Z);
/// <summary>
/// Send an entity interaction packet to the server.
@ -146,7 +154,7 @@ namespace MinecraftClient.Protocol
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
/// <param name="hand">Only if Type is interact or interact at; 0: main hand, 1: off hand</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type, int hand);
Task<bool> SendInteractEntity(int EntityID, int type, int hand);
/// <summary>
/// Send a use item packet to the server
@ -154,7 +162,7 @@ namespace MinecraftClient.Protocol
/// <param name="hand">0: main hand, 1: off hand</param>
/// <param name="sequenceId">Sequence ID used for synchronization</param>
/// <returns>True if packet was successfully sent</returns>
bool SendUseItem(int hand, int sequenceId);
Task<bool> SendUseItem(int hand, int sequenceId);
/// <summary>
/// Send a click window slot packet to the server
@ -166,7 +174,7 @@ namespace MinecraftClient.Protocol
/// <param name="changedSlots">Slots that have been changed in this event: List<SlotID, Changed Items> </param>
/// <param name="stateId">Inventory's stateId</param>
/// <returns>True if packet was successfully sent</returns>
bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List<Tuple<short, Item?>> changedSlots, int stateId);
Task<bool> SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List<Tuple<short, Item?>> changedSlots, int stateId);
/// <summary>
/// Request Creative Mode item creation into regular/survival Player Inventory
@ -177,7 +185,7 @@ namespace MinecraftClient.Protocol
/// <param name="count">Item count</param>
/// <param name="nbt">Optional item NBT</param>
/// <returns>TRUE if item given successfully</returns>
bool SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt);
Task<bool> SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt);
/// <summary>
/// Send a click container button packet to the server.
@ -187,7 +195,7 @@ namespace MinecraftClient.Protocol
/// <param name="buttonId">Id of the clicked button</param>
/// <returns>True if packet was successfully sent</returns>
bool ClickContainerButton(int windowId, int buttonId);
Task<bool> ClickContainerButton(int windowId, int buttonId);
/// <summary>
/// Plays animation
@ -195,13 +203,13 @@ namespace MinecraftClient.Protocol
/// <param name="animation">0 for left arm, 1 for right arm</param>
/// <param name="playerid">Player Entity ID</param>
/// <returns>TRUE if item given successfully</returns>
bool SendAnimation(int animation, int playerid);
Task<bool> SendAnimation(int animation, int playerid);
/// <summary>
/// Send a close window packet to the server
/// </summary>
/// <param name="windowId">Id of the window being closed</param>
bool SendCloseWindow(int windowId);
Task<bool> SendCloseWindow(int windowId);
/// <summary>
/// Send player block placement packet to the server
@ -211,7 +219,7 @@ namespace MinecraftClient.Protocol
/// <param name="face">Block face</param>
/// <param name="sequenceId">Sequence ID (use for synchronization)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId);
Task<bool> SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId);
/// <summary>
/// Send player blog digging packet to the server. This packet needs to be called at least twice: Once to begin digging, then a second time to finish digging
@ -221,7 +229,7 @@ namespace MinecraftClient.Protocol
/// <param name="face">Block face</param>
/// <param name="sequenceId">Sequence ID (use for synchronization)</param>
/// <returns>True if packet was succcessfully sent</returns>
bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId);
Task<bool> SendPlayerDigging(int status, Location location, Direction face, int sequenceId);
/// <summary>
/// Change text on a sign
@ -232,7 +240,7 @@ namespace MinecraftClient.Protocol
/// <param name="line3">New line 3</param>
/// <param name="line4">New line 4</param>
/// <returns>True if packet was succcessfully sent</returns>
bool SendUpdateSign(Location location, string line1, string line2, string line3, string line4);
Task<bool> SendUpdateSign(Location location, string line1, string line2, string line3, string line4);
/// <summary>
/// Update command block
@ -241,24 +249,18 @@ namespace MinecraftClient.Protocol
/// <param name="command">command</param>
/// <param name="mode">command block mode</param>
/// <param name="flags">command block flags</param>
bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags);
Task<bool> UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags);
/// <summary>
/// Select villager trade
/// </summary>
/// <param name="selectedSlot">The slot of the trade, starts at 0.</param>
bool SelectTrade(int selectedSlot);
Task<bool> SelectTrade(int selectedSlot);
/// <summary>
/// Spectate a player/entity
/// </summary>
/// <param name="uuid">The uuid of the player/entity to spectate/teleport to.</param>
bool SendSpectate(Guid uuid);
/// <summary>
/// Get net read thread (main thread) ID
/// </summary>
/// <returns>Net read thread ID</returns>
int GetNetMainThreadId();
Task<bool> SendSpectate(Guid uuid);
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MinecraftClient.Inventory;
using MinecraftClient.Logger;
using MinecraftClient.Mapping;
@ -18,7 +19,6 @@ namespace MinecraftClient.Protocol
{
/* The MinecraftCom Handler must
* provide these getters */
int GetServerPort();
string GetServerHost();
string GetUsername();
@ -43,26 +43,6 @@ namespace MinecraftClient.Protocol
Container? GetInventory(int inventoryID);
ILogger GetLogger();
/// <summary>
/// Invoke a task on the main thread, wait for completion and retrieve return value.
/// </summary>
/// <param name="task">Task to run with any type or return value</param>
/// <returns>Any result returned from task, result type is inferred from the task</returns>
/// <example>bool result = InvokeOnMainThread(methodThatReturnsAbool);</example>
/// <example>bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument));</example>
/// <example>int result = InvokeOnMainThread(() => { yourCode(); return 42; });</example>
/// <typeparam name="T">Type of the return value</typeparam>
T InvokeOnMainThread<T>(Func<T> task);
/// <summary>
/// Invoke a task on the main thread and wait for completion
/// </summary>
/// <param name="task">Task to run without return value</param>
/// <example>InvokeOnMainThread(methodThatReturnsNothing);</example>
/// <example>InvokeOnMainThread(() => methodThatReturnsNothing(argument));</example>
/// <example>InvokeOnMainThread(() => { yourCode(); });</example>
void InvokeOnMainThread(Action task);
/// <summary>
/// Called when a network packet received or sent
/// </summary>
@ -78,7 +58,7 @@ namespace MinecraftClient.Protocol
/// <summary>
/// Called when a server was successfully joined
/// </summary>
void OnGameJoined();
Task OnGameJoined();
/// <summary>
/// Received chat/system message from the server
@ -184,21 +164,21 @@ namespace MinecraftClient.Protocol
/// Called ~10 times per second (10 ticks per second)
/// Useful for updating bots in other parts of the program
/// </summary>
void OnUpdate();
Task OnUpdate();
/// <summary>
/// Registers the given plugin channel for the given bot.
/// </summary>
/// <param name="channel">The channel to register.</param>
/// <param name="bot">The bot to register the channel for.</param>
void RegisterPluginChannel(string channel, ChatBot bot);
Task RegisterPluginChannel(string channel, ChatBot bot);
/// <summary>
/// Unregisters the given plugin channel for the given bot.
/// </summary>
/// <param name="channel">The channel to unregister.</param>
/// <param name="bot">The bot to unregister the channel for.</param>
void UnregisterPluginChannel(string channel, ChatBot bot);
Task UnregisterPluginChannel(string channel, ChatBot bot);
/// <summary>
/// Sends a plugin channel packet to the server.
@ -208,7 +188,7 @@ namespace MinecraftClient.Protocol
/// <param name="data">The payload for the packet.</param>
/// <param name="sendEvenIfNotRegistered">Whether the packet should be sent even if the server or the client hasn't registered it yet.</param>
/// <returns>Whether the packet was sent: true if it was sent, false if there was a connection error or it wasn't registered.</returns>
bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false);
Task<bool> SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false);
/// <summary>
/// Called when a plugin channel message was sent from the server.
@ -348,7 +328,7 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="uuid">Affected player's UUID</param>
/// <param name="gamemode">New game mode</param>
void OnGamemodeUpdate(Guid uuid, int gamemode);
Task OnGamemodeUpdate(Guid uuid, int gamemode);
/// <summary>
/// Called when a player's latency has changed
@ -472,6 +452,6 @@ namespace MinecraftClient.Protocol
/// <param name="buttonId">Id of the clicked button</param>
/// <returns>True if packet was successfully sent</returns>
bool ClickContainerButton(int windowId, int buttonId);
public Task<bool> ClickContainerButton(int windowId, int buttonId);
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Text;
namespace MinecraftClient.Protocol
@ -6,11 +7,10 @@ namespace MinecraftClient.Protocol
// Thanks to https://stackoverflow.com/questions/60404612/parse-jwt-token-to-get-the-payload-content-only-without-external-library-in-c-sh
public static class JwtPayloadDecode
{
public static string GetPayload(string token)
public static MemoryStream GetPayload(string token)
{
var content = token.Split('.')[1];
var jsonPayload = Encoding.UTF8.GetString(Decode(content));
return jsonPayload;
return new MemoryStream(Decode(content));
}
private static byte[] Decode(string input)
@ -23,7 +23,7 @@ namespace MinecraftClient.Protocol
case 0: break; // No pad chars in this case
case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char
default: throw new System.ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!");
default: throw new ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!");
}
var converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted;

View file

@ -3,9 +3,18 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MinecraftClient.Protocol.ProfileKey;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig;
@ -13,9 +22,10 @@ namespace MinecraftClient.Protocol
{
static class Microsoft
{
private static readonly string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3";
private static readonly string signinUrl = string.Format("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={0}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment", clientId);
private static readonly string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
private const string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3";
private const string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
private const string signinUrl = $"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment";
private const string certificates = "https://api.minecraftservices.com/player/certificates";
public static string SignInUrl { get { return signinUrl; } }
@ -26,7 +36,7 @@ namespace MinecraftClient.Protocol
/// <returns>Sign-in URL with email pre-filled</returns>
public static string GetSignInUrlWithHint(string loginHint)
{
return SignInUrl + "&login_hint=" + Uri.EscapeDataString(loginHint);
return $"{SignInUrl}&login_hint={Uri.EscapeDataString(loginHint)}";
}
/// <summary>
@ -34,11 +44,16 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="code">Auth code obtained after user signing in</param>
/// <returns>Access token and refresh token</returns>
public static LoginResponse RequestAccessToken(string code)
public static async Task<LoginResponse> RequestAccessTokenAsync(HttpClient httpClient, string code)
{
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
postData = string.Format(postData, clientId, code);
return RequestToken(postData);
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
{
new("client_id", clientId),
new("grant_type", "authorization_code"),
new("redirect_uri", "https://mccteam.github.io/redirect.html"),
new("code", code),
});
return await RequestTokenAsync(httpClient, postData);
}
/// <summary>
@ -46,11 +61,43 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="refreshToken">Refresh token</param>
/// <returns>Access token and new refresh token</returns>
public static LoginResponse RefreshAccessToken(string refreshToken)
public static async Task<LoginResponse> RefreshAccessTokenAsync(HttpClient httpClient, string refreshToken)
{
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
postData = string.Format(postData, clientId, refreshToken);
return RequestToken(postData);
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
{
new("client_id", clientId),
new("grant_type", "refresh_token"),
new("redirect_uri", "https://mccteam.github.io/redirect.html"),
new("refresh_token", refreshToken),
});
return await RequestTokenAsync(httpClient, postData);
}
private record TokenInfo
{
public string? token_type { init; get; }
public string? scope { init; get; }
public int expires_in { init; get; }
public int ext_expires_in { init; get; }
public string? access_token { init; get; }
public string? refresh_token { init; get; }
public string? id_token { init; get; }
public string? error { init; get; }
public string? error_description { init; get; }
}
private record JwtPayloadInIdToken
{
public string? ver { init; get; }
public string? iss { init; get; }
public string? sub { init; get; }
public string? aud { init; get; }
public long exp { init; get; }
public long iat { init; get; }
public long nbf { init; get; }
public string? email { init; get; }
public string? tid { init; get; }
public string? aio { init; get; }
}
/// <summary>
@ -58,45 +105,88 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="postData">Complete POST data for the request</param>
/// <returns></returns>
private static LoginResponse RequestToken(string postData)
private static async Task<LoginResponse> RequestTokenAsync(HttpClient httpClient, FormUrlEncodedContent postData)
{
var request = new ProxiedWebRequest(tokenUrl)
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var jsonData = Json.ParseJson(response.Body);
using HttpResponseMessage response = await httpClient.PostAsync(tokenUrl, postData);
TokenInfo jsonData = (await response.Content.ReadFromJsonAsync<TokenInfo>())!;
// Error handling
if (jsonData.Properties.ContainsKey("error"))
if (!string.IsNullOrEmpty(jsonData.error))
{
throw new Exception(jsonData.Properties["error_description"].StringValue);
throw new Exception(jsonData.error_description);
}
else
{
string accessToken = jsonData.Properties["access_token"].StringValue;
string refreshToken = jsonData.Properties["refresh_token"].StringValue;
int expiresIn = int.Parse(jsonData.Properties["expires_in"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
// Extract email from JWT
string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue);
var jsonPayload = Json.ParseJson(payload);
string email = jsonPayload.Properties["email"].StringValue;
Stream payload = JwtPayloadDecode.GetPayload(jsonData.id_token!);
JwtPayloadInIdToken jsonPayload = (await JsonSerializer.DeserializeAsync<JwtPayloadInIdToken>(payload))!;
return new LoginResponse()
{
Email = email,
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = expiresIn
Email = jsonPayload.email!,
AccessToken = jsonData.access_token!,
RefreshToken = jsonData.refresh_token!,
ExpiresIn = jsonData.expires_in,
};
}
}
private record ProfileKeyResult
{
public KeyPair? keyPair { init; get; }
public string? publicKeySignature { init; get; }
public string? publicKeySignatureV2 { init; get; }
public DateTime expiresAt { init; get; }
public DateTime refreshedAfter { init; get; }
public record KeyPair
{
public string? privateKey { init; get; }
public string? publicKey { init; get; }
}
}
/// <summary>
/// Request the key to be used for message signing.
/// </summary>
/// <param name="accessToken">Access token in session</param>
/// <returns>Profile key</returns>
public static async Task<PlayerKeyPair?> RequestProfileKeyAsync(HttpClient httpClient, string accessToken)
{
try
{
using HttpRequestMessage request = new(HttpMethod.Post, certificates);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
using HttpResponseMessage response = await httpClient.SendAsync(request);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
ProfileKeyResult jsonData = (await response.Content.ReadFromJsonAsync<ProfileKeyResult>())!;
PublicKey publicKey = new(jsonData.keyPair!.publicKey!, jsonData.publicKeySignature, jsonData.publicKeySignatureV2);
PrivateKey privateKey = new(jsonData.keyPair!.privateKey!);
return new PlayerKeyPair(publicKey, privateKey, jsonData.expiresAt, jsonData.refreshedAfter);
}
catch (HttpRequestException e)
{
ConsoleIO.WriteLineFormatted("§cFetch profile key failed: " + e.Message);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
return null;
}
}
public static void OpenBrowser(string link)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (OperatingSystem.IsWindows())
{
var ps = new ProcessStartInfo(link)
{
@ -106,11 +196,11 @@ namespace MinecraftClient.Protocol
Process.Start(ps);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
else if (OperatingSystem.IsLinux())
{
Process.Start("xdg-open", link);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
else if (OperatingSystem.IsMacOS())
{
Process.Start("open", link);
}
@ -134,53 +224,86 @@ namespace MinecraftClient.Protocol
}
}
static class XboxLive
static partial class XboxLive
{
private static readonly string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en";
private static readonly string xbl = "https://user.auth.xboxlive.com/user/authenticate";
private static readonly string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
internal const string UserAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
private static readonly string userAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
private const string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
private const string xbl = "https://user.auth.xboxlive.com/user/authenticate";
private const string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en";
private static readonly Regex ppft = new("sFTTag:'.*value=\"(.*)\"\\/>'");
private static readonly Regex urlPost = new("urlPost:'(.+?(?=\'))");
private static readonly Regex confirm = new("identity\\/confirm");
private static readonly Regex invalidAccount = new("Sign in to", RegexOptions.IgnoreCase);
private static readonly Regex twoFA = new("Help us protect your account", RegexOptions.IgnoreCase);
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
{
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = false,
ReadCommentHandling = JsonCommentHandling.Skip,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
public static string SignInUrl { get { return authorize; } }
private record AuthPayload
{
public Propertie? Properties { init; get; }
public string? RelyingParty { init; get; }
public string? TokenType { init; get; }
public record Propertie
{
public string? AuthMethod { init; get; }
public string? SiteName { init; get; }
public string? RpsTicket { init; get; }
public string? SandboxId { init; get; }
public string[]? UserTokens { init; get; }
}
}
private record AuthResult
{
public DateTime IssueInstant { init; get; }
public DateTime NotAfter { init; get; }
public string? Token { init; get; }
public DisplayClaim? DisplayClaims { init; get; }
public record DisplayClaim
{
public Dictionary<string, string>[]? xui { init; get; }
}
}
private record AuthError
{
public string? Identity { init; get; }
public long XErr { init; get; }
public string? Message { init; get; }
public string? Redirect { init; get; }
}
/// <summary>
/// Pre-authentication
/// </summary>
/// <remarks>This step is to get the login page for later use</remarks>
/// <returns></returns>
public static PreAuthResponse PreAuth()
public static async Task<PreAuthResponse> PreAuthAsync(HttpClient httpClient)
{
var request = new ProxiedWebRequest(authorize)
{
UserAgent = userAgent
};
var response = request.Get();
using HttpResponseMessage response = await httpClient.GetAsync(authorize);
string html = response.Body;
string html = await response.Content.ReadAsStringAsync();
string PPFT = ppft.Match(html).Groups[1].Value;
string urlPost = XboxLive.urlPost.Match(html).Groups[1].Value;
string PPFT = GetPpftRegex().Match(html).Groups[1].Value;
string urlPost = GetUrlPostRegex().Match(html).Groups[1].Value;
if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost))
{
throw new Exception("Fail to extract PPFT or urlPost");
}
//Console.WriteLine("PPFT: {0}", PPFT);
//Console.WriteLine();
//Console.WriteLine("urlPost: {0}", urlPost);
return new PreAuthResponse()
{
UrlPost = urlPost,
PPFT = PPFT,
Cookie = response.Cookies
Cookie = new()// response.Cookies
};
}
@ -192,69 +315,54 @@ namespace MinecraftClient.Protocol
/// <param name="password">Account password</param>
/// <param name="preAuth"></param>
/// <returns></returns>
public static Microsoft.LoginResponse UserLogin(string email, string password, PreAuthResponse preAuth)
public static async Task<Microsoft.LoginResponse> UserLoginAsync(HttpClient httpClient, string email, string password, PreAuthResponse preAuth)
{
var request = new ProxiedWebRequest(preAuth.UrlPost, preAuth.Cookie)
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
{
UserAgent = userAgent
};
new("login", email),
new("loginfmt", email),
new("passwd", password),
new("PPFT", preAuth.PPFT),
});
string postData = "login=" + Uri.EscapeDataString(email)
+ "&loginfmt=" + Uri.EscapeDataString(email)
+ "&passwd=" + Uri.EscapeDataString(password)
+ "&PPFT=" + Uri.EscapeDataString(preAuth.PPFT);
using HttpResponseMessage response = await httpClient.PostAsync(preAuth.UrlPost, postData);
var response = request.Post("application/x-www-form-urlencoded", postData);
if (Settings.Config.Logging.DebugMessages)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode >= 300 && response.StatusCode <= 399)
if (response.IsSuccessStatusCode)
{
string url = response.Headers.Get("Location")!;
string hash = url.Split('#')[1];
var request2 = new ProxiedWebRequest(url);
var response2 = request2.Get();
if (response2.StatusCode != 200)
{
throw new Exception("Authentication failed");
}
string hash = response.RequestMessage!.RequestUri!.Fragment[1..];
if (string.IsNullOrEmpty(hash))
{
throw new Exception("Cannot extract access token");
}
var dict = Request.ParseQueryString(hash);
//foreach (var pair in dict)
//{
// Console.WriteLine("{0}: {1}", pair.Key, pair.Value);
//}
var dict = Request.ParseQueryString(hash);
return new Microsoft.LoginResponse()
{
Email = email,
AccessToken = dict["access_token"],
RefreshToken = dict["refresh_token"],
ExpiresIn = int.Parse(dict["expires_in"], NumberStyles.Any, CultureInfo.CurrentCulture)
ExpiresIn = int.Parse(dict["expires_in"])
};
}
else
{
if (twoFA.IsMatch(response.Body))
string body = await response.Content.ReadAsStringAsync();
if (GetTwoFARegex().IsMatch(body))
{
// TODO: Handle 2FA
throw new Exception("2FA enabled but not supported yet. Use browser sign-in method or try to disable 2FA in Microsoft account settings");
}
else if (invalidAccount.IsMatch(response.Body))
else if (GetInvalidAccountRegex().IsMatch(body))
{
throw new Exception("Invalid credentials. Check your credentials");
}
else throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode);
else
{
throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode);
}
}
}
@ -263,54 +371,54 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="loginResponse"></param>
/// <returns></returns>
public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse)
public static async Task<XblAuthenticateResponse> XblAuthenticateAsync(HttpClient httpClient, Microsoft.LoginResponse loginResponse)
{
var request = new ProxiedWebRequest(xbl)
{
UserAgent = userAgent,
Accept = "application/json"
};
request.Headers.Add("x-xbl-contract-version", "0");
var accessToken = loginResponse.AccessToken;
string accessToken;
if (Config.Main.General.Method == LoginMethod.browser)
{
// Our own client ID must have d= in front of the token or HTTP status 400
// "Stolen" client ID must not have d= in front of the token or HTTP status 400
accessToken = "d=" + accessToken;
accessToken = "d=" + loginResponse.AccessToken;
}
else
{
accessToken = loginResponse.AccessToken;
}
string payload = "{"
+ "\"Properties\": {"
+ "\"AuthMethod\": \"RPS\","
+ "\"SiteName\": \"user.auth.xboxlive.com\","
+ "\"RpsTicket\": \"" + accessToken + "\""
+ "},"
+ "\"RelyingParty\": \"http://auth.xboxlive.com\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
if (Settings.Config.Logging.DebugMessages)
AuthPayload payload = new()
{
Properties = new AuthPayload.Propertie()
{
AuthMethod = "RPS",
SiteName = "user.auth.xboxlive.com",
RpsTicket = accessToken,
},
RelyingParty = "http://auth.xboxlive.com",
TokenType = "JWT",
};
using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");
httpContent.Headers.Add("x-xbl-contract-version", "0");
using HttpResponseMessage response = await httpClient.PostAsync(xbl, httpContent);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200)
{
string jsonString = response.Body;
//Console.WriteLine(jsonString);
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
if (response.IsSuccessStatusCode)
{
AuthResult jsonData = (await response.Content.ReadFromJsonAsync<AuthResult>())!;
return new XblAuthenticateResponse()
{
Token = token,
UserHash = userHash
Token = jsonData.Token!,
UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
};
}
else
{
throw new Exception("XBL Authentication failed");
throw new Exception("XBL Authentication failed, code = " + response.StatusCode.ToString());
}
}
@ -320,56 +428,53 @@ namespace MinecraftClient.Protocol
/// <remarks>(Don't ask me what is XSTS, I DONT KNOW)</remarks>
/// <param name="xblResponse"></param>
/// <returns></returns>
public static XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse)
public static async Task<XSTSAuthenticateResponse> XSTSAuthenticateAsync(HttpClient httpClient, XblAuthenticateResponse xblResponse)
{
var request = new ProxiedWebRequest(xsts)
AuthPayload payload = new()
{
UserAgent = userAgent,
Accept = "application/json"
Properties = new AuthPayload.Propertie()
{
SandboxId = "RETAIL",
UserTokens = new string[] { xblResponse.Token },
},
RelyingParty = "rp://api.minecraftservices.com/",
TokenType = "JWT",
};
request.Headers.Add("x-xbl-contract-version", "1");
string payload = "{"
+ "\"Properties\": {"
+ "\"SandboxId\": \"RETAIL\","
+ "\"UserTokens\": ["
+ "\"" + xblResponse.Token + "\""
+ "]"
+ "},"
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
if (Settings.Config.Logging.DebugMessages)
{
using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");
httpContent.Headers.Add("x-xbl-contract-version", "1");
using HttpResponseMessage response = await httpClient.PostAsync(xsts, httpContent);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200)
if (response.IsSuccessStatusCode)
{
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
AuthResult jsonData = (await response.Content.ReadFromJsonAsync<AuthResult>())!;
return new XSTSAuthenticateResponse()
{
Token = token,
UserHash = userHash
Token = jsonData.Token!,
UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
};
}
else
{
if (response.StatusCode == 401)
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Json.JSONData json = Json.ParseJson(response.Body);
if (json.Properties["XErr"].StringValue == "2148916233")
{
AuthError jsonData = (await response.Content.ReadFromJsonAsync<AuthError>())!;
if (jsonData.XErr == 2148916233)
throw new Exception("The account doesn't have an Xbox account");
}
else if (json.Properties["XErr"].StringValue == "2148916238")
{
else if (jsonData.XErr == 2148916235)
throw new Exception("The account is from a country where Xbox Live is not available/banned");
else if (jsonData.XErr == 2148916236 || jsonData.XErr == 2148916237)
throw new Exception("The account needs adult verification on Xbox page. (South Korea)");
else if (jsonData.XErr == 2148916238)
throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult");
}
else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue);
else
throw new Exception("Unknown XSTS error code: " + jsonData.XErr.ToString() + ", Check " + jsonData.Redirect);
}
else
{
@ -396,13 +501,63 @@ namespace MinecraftClient.Protocol
public string Token;
public string UserHash;
}
[GeneratedRegex("sFTTag:'.*value=\"(.*)\"\\/>'")]
private static partial Regex GetPpftRegex();
[GeneratedRegex("urlPost:'(.+?(?='))")]
private static partial Regex GetUrlPostRegex();
[GeneratedRegex("identity\\/confirm")]
private static partial Regex GetConfirmRegex();
[GeneratedRegex("Sign in to", RegexOptions.IgnoreCase, "zh-CN")]
private static partial Regex GetInvalidAccountRegex();
[GeneratedRegex("Help us protect your account", RegexOptions.IgnoreCase, "zh-CN")]
private static partial Regex GetTwoFARegex();
}
static class MinecraftWithXbox
{
private static readonly string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox";
private static readonly string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
private static readonly string profile = "https://api.minecraftservices.com/minecraft/profile";
private const string profile = "https://api.minecraftservices.com/minecraft/profile";
private const string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
private const string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox";
private record LoginPayload
{
public string? identityToken { init; get; }
}
private record LoginResult
{
public string? username { init; get; }
public string[]? roles { init; get; }
public string? access_token { init; get; }
public string? token_type { init; get; }
public int expires_in { init; get; }
}
private record GameOwnershipResult
{
public Dictionary<string, string>[]? items { init; get; }
public string? signature { init; get; }
public string? keyId { init; get; }
}
private record GameProfileResult
{
public string? id { init; get; }
public string? name { init; get; }
public Dictionary<string, string>[]? skins { init; get; }
public Dictionary<string, string>[]? capes { init; get; }
/* Error */
public string? path { init; get; }
public string? errorType { init; get; }
public string? error { init; get; }
public string? errorMessage { init; get; }
public string? developerMessage { init; get; }
}
/// <summary>
/// Login to Minecraft using the XSTS token and user hash obtained before
@ -410,25 +565,23 @@ namespace MinecraftClient.Protocol
/// <param name="userHash"></param>
/// <param name="xstsToken"></param>
/// <returns></returns>
public static string LoginWithXbox(string userHash, string xstsToken)
public static async Task<string> LoginWithXboxAsync(HttpClient httpClient, string userHash, string xstsToken)
{
var request = new ProxiedWebRequest(loginWithXbox)
LoginPayload payload = new()
{
Accept = "application/json"
identityToken = $"XBL3.0 x={userHash};{xstsToken}",
};
string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}";
var response = request.Post("application/json", payload);
using StringContent httpContent = new(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
if (Settings.Config.Logging.DebugMessages)
{
using HttpResponseMessage response = await httpClient.PostAsync(loginWithXbox, httpContent);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
LoginResult jsonData = (await response.Content.ReadFromJsonAsync<LoginResult>())!;
return json.Properties["access_token"].StringValue;
return jsonData.access_token!;
}
/// <summary>
@ -436,39 +589,40 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="accessToken"></param>
/// <returns>True if the user own the game</returns>
public static bool UserHasGame(string accessToken)
public static async Task<bool> CheckUserHasGameAsync(HttpClient httpClient, string accessToken)
{
var request = new ProxiedWebRequest(ownership);
using HttpRequestMessage request = new(HttpMethod.Get, ownership);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
if (Settings.Config.Logging.DebugMessages)
{
using HttpResponseMessage response = await httpClient.SendAsync(request);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["items"].DataArray.Count > 0;
GameOwnershipResult jsonData = (await response.Content.ReadFromJsonAsync<GameOwnershipResult>())!;
return jsonData.items!.Length > 0;
}
public static UserProfile GetUserProfile(string accessToken)
public static async Task<UserProfile> GetUserProfileAsync(HttpClient httpClient, string accessToken)
{
var request = new ProxiedWebRequest(profile);
using HttpRequestMessage request = new(HttpMethod.Get, profile);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
if (Settings.Config.Logging.DebugMessages)
{
using HttpResponseMessage response = await httpClient.SendAsync(request);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
GameProfileResult jsonData = (await response.Content.ReadFromJsonAsync<GameProfileResult>())!;
if (!string.IsNullOrEmpty(jsonData.error))
throw new Exception($"{jsonData.errorType}: {jsonData.error}. {jsonData.errorMessage}");
return new UserProfile()
{
UUID = json.Properties["id"].StringValue,
UserName = json.Properties["name"].StringValue
UUID = jsonData.id!,
UserName = jsonData.name!,
};
}

View file

@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using MinecraftClient.Crypto.AesHandler;
using static ConsoleInteractive.ConsoleReader;
namespace MinecraftClient.Protocol.PacketPipeline
{
public class AesStream : Stream
{
public const int BlockSize = 16;
private const int BufferSize = 1024;
public Socket Client;
private bool inStreamEnded = false;
private readonly IAesHandler Aes;
private int InputBufPos = 0, OutputBufPos = 0;
private readonly Memory<byte> InputBuf, OutputBuf;
private readonly Memory<byte> AesBufRead, AesBufSend;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public AesStream(Socket socket, byte[] key)
{
Client = socket;
InputBuf = new byte[BufferSize + BlockSize];
OutputBuf = new byte[BufferSize + BlockSize];
AesBufRead = new byte[BlockSize];
AesBufSend = new byte[BlockSize];
if (FasterAesX86.IsSupported())
Aes = new FasterAesX86(key);
else if (FasterAesArm.IsSupported())
Aes = new FasterAesArm(key);
else
Aes = new BasicAes(key);
key.CopyTo(InputBuf.Slice(0, BlockSize));
key.CopyTo(OutputBuf.Slice(0, BlockSize));
}
public override void Flush()
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
var task = ReadAsync(buffer.AsMemory(offset, count)).AsTask();
task.Wait();
return task.Result;
}
public override int ReadByte()
{
if (inStreamEnded)
return -1;
var task = Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, 1)).AsTask();
task.Wait();
if (task.Result == 0)
{
inStreamEnded = true;
return -1;
}
Aes.EncryptEcb(InputBuf.Slice(InputBufPos, BlockSize).Span, AesBufRead.Span);
byte result = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize]);
InputBufPos++;
if (InputBufPos == BufferSize)
{
InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf[..BlockSize]);
InputBufPos = 0;
}
return result;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (inStreamEnded)
return 0;
int readLimit = Math.Min(buffer.Length, BufferSize - InputBufPos);
int curRead = await Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, readLimit), cancellationToken);
if (curRead == 0 || cancellationToken.IsCancellationRequested)
{
if (curRead == 0)
inStreamEnded = true;
return curRead;
}
for (int idx = 0; idx < curRead; idx++)
{
Aes.EncryptEcb(InputBuf.Slice(InputBufPos + idx, BlockSize).Span, AesBufRead.Span);
buffer.Span[idx] = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize + idx]);
}
InputBufPos += curRead;
if (InputBufPos == BufferSize)
{
InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf[..BlockSize]);
InputBufPos = 0;
}
return curRead;
}
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (inStreamEnded)
return;
for (int readed = 0, curRead; readed < buffer.Length; readed += curRead)
{
int readLimit = Math.Min(buffer.Length - readed, BufferSize - InputBufPos);
curRead = await Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, readLimit), cancellationToken);
if (curRead == 0 || cancellationToken.IsCancellationRequested)
{
if (curRead == 0)
inStreamEnded = true;
return;
}
for (int idx = 0; idx < curRead; idx++)
{
Aes.EncryptEcb(InputBuf.Slice(InputBufPos + idx, BlockSize).Span, AesBufRead.Span);
buffer.Span[readed + idx] = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize + idx]);
}
InputBufPos += curRead;
if (InputBufPos == BufferSize)
{
InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf.Slice(0, BlockSize));
InputBufPos = 0;
}
}
}
public async ValueTask<int> ReadRawAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
return await Client.ReceiveAsync(buffer, cancellationToken);
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer.AsMemory(offset, count)).AsTask().Wait();
}
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
int outputStartPos = OutputBufPos;
for (int wirtten = 0; wirtten < buffer.Length; ++wirtten)
{
if (cancellationToken.IsCancellationRequested)
return;
Aes.EncryptEcb(OutputBuf.Slice(OutputBufPos, BlockSize).Span, AesBufSend.Span);
OutputBuf.Span[OutputBufPos + BlockSize] = (byte)(AesBufSend.Span[0] ^ buffer.Span[wirtten]);
if (++OutputBufPos == BufferSize)
{
await Client.SendAsync(OutputBuf.Slice(outputStartPos + BlockSize, BufferSize - outputStartPos), cancellationToken);
OutputBuf.Slice(BufferSize, BlockSize).CopyTo(OutputBuf.Slice(0, BlockSize));
OutputBufPos = outputStartPos = 0;
}
}
if (OutputBufPos > outputStartPos)
await Client.SendAsync(OutputBuf.Slice(outputStartPos + BlockSize, OutputBufPos - outputStartPos), cancellationToken);
return;
}
}
}

View file

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static ConsoleInteractive.ConsoleReader;
namespace MinecraftClient.Protocol.PacketPipeline
{
internal class PacketStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
readonly CancellationToken CancelToken;
private readonly Stream baseStream;
private readonly AesStream? aesStream;
private ZLibStream? zlibStream;
private int packetSize, packetReaded;
internal const int DropBufSize = 1024;
internal static readonly Memory<byte> DropBuf = new byte[DropBufSize];
private static readonly byte[] SingleByteBuf = new byte[1];
public PacketStream(ZLibStream zlibStream, int packetSize, CancellationToken cancellationToken = default)
{
CancelToken = cancellationToken;
this.aesStream = null;
this.zlibStream = zlibStream;
this.baseStream = zlibStream;
this.packetReaded = 0;
this.packetSize = packetSize;
}
public PacketStream(AesStream aesStream, int packetSize, CancellationToken cancellationToken = default)
{
CancelToken = cancellationToken;
this.aesStream = aesStream;
this.zlibStream = null;
this.baseStream = aesStream;
this.packetReaded = 0;
this.packetSize = packetSize;
}
public PacketStream(Stream baseStream, int packetSize, CancellationToken cancellationToken = default)
{
CancelToken = cancellationToken;
this.aesStream = null;
this.zlibStream = null;
this.baseStream = baseStream;
this.packetReaded = 0;
this.packetSize = packetSize;
}
public override void Flush()
{
throw new NotSupportedException();
}
public new byte ReadByte()
{
++packetReaded;
if (packetReaded > packetSize)
throw new OverflowException("Reach the end of the packet!");
baseStream.Read(SingleByteBuf, 0, 1);
return SingleByteBuf[0];
}
public async Task<byte> ReadByteAsync()
{
++packetReaded;
if (packetReaded > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(SingleByteBuf, CancelToken);
return SingleByteBuf[0];
}
public override int Read(byte[] buffer, int offset, int count)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
int readed = baseStream.Read(buffer, offset, count);
packetReaded += readed;
return readed;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
int readed = await baseStream.ReadAsync(buffer, CancelToken);
packetReaded += readed;
return readed;
}
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(buffer, CancelToken);
packetReaded += buffer.Length;
}
public async Task<byte[]> ReadFullPacket()
{
byte[] buffer = new byte[packetSize - packetReaded];
await ReadExactlyAsync(buffer);
packetReaded = packetSize;
return buffer;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public async Task Skip(int length)
{
if (zlibStream != null)
{
for (int readed = 0, curRead; readed < length; readed += curRead)
curRead = await zlibStream.ReadAsync(DropBuf[..Math.Min(DropBufSize, length - readed)]);
}
else if (aesStream != null)
{
int skipRaw = length - AesStream.BlockSize;
for (int readed = 0, curRead; readed < skipRaw; readed += curRead)
curRead = await aesStream.ReadRawAsync(DropBuf[..Math.Min(DropBufSize, skipRaw - readed)]);
await aesStream.ReadAsync(DropBuf[..Math.Min(length, AesStream.BlockSize)]);
}
else
{
for (int readed = 0, curRead; readed < length; readed += curRead)
curRead = await baseStream.ReadAsync(DropBuf[..Math.Min(DropBufSize, length - readed)]);
}
packetReaded += length;
}
public override async ValueTask DisposeAsync()
{
if (CancelToken.IsCancellationRequested)
return;
if (zlibStream != null)
{
await zlibStream.DisposeAsync();
zlibStream = null;
packetReaded = packetSize;
}
else
{
if (packetSize - packetReaded > 0)
{
// ConsoleIO.WriteLine("Plain readed " + packetReaded + ", last " + (packetSize - packetReaded));
await Skip(packetSize - packetReaded);
}
}
}
}
}

View file

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
namespace MinecraftClient.Protocol.PacketPipeline
{
/// <summary>
/// Wrapper for handling unencrypted & encrypted socket
/// </summary>
class SocketWrapper
{
private TcpClient tcpClient;
private AesStream? AesStream;
private PacketStream? packetStream = null;
private Stream ReadStream, WriteStream;
private bool Encrypted = false;
public int CompressionThreshold { get; set; } = 0;
private SemaphoreSlim SendSemaphore = new SemaphoreSlim(1, 1);
private Task LastSendTask = Task.CompletedTask;
/// <summary>
/// Initialize a new SocketWrapper
/// </summary>
/// <param name="client">TcpClient connected to the server</param>
public SocketWrapper(TcpClient client)
{
tcpClient = client;
ReadStream = WriteStream = client.GetStream();
}
/// <summary>
/// Check if the socket is still connected
/// </summary>
/// <returns>TRUE if still connected</returns>
/// <remarks>Silently dropped connection can only be detected by attempting to read/write data</remarks>
public bool IsConnected()
{
return tcpClient.Client != null && tcpClient.Connected;
}
/// <summary>
/// Check if the socket has data available to read
/// </summary>
/// <returns>TRUE if data is available to read</returns>
public bool HasDataAvailable()
{
return tcpClient.Client.Available > 0;
}
/// <summary>
/// Switch network reading/writing to an encrypted stream
/// </summary>
/// <param name="secretKey">AES secret key</param>
public void SwitchToEncrypted(byte[] secretKey)
{
if (Encrypted)
throw new InvalidOperationException("Stream is already encrypted!?");
Encrypted = true;
ReadStream = WriteStream = AesStream = new AesStream(tcpClient.Client, secretKey);
}
/// <summary>
/// Send raw data to the server.
/// </summary>
/// <param name="buffer">data to send</param>
public async Task SendAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
await SendSemaphore.WaitAsync();
await LastSendTask;
LastSendTask = WriteStream.WriteAsync(buffer, cancellationToken).AsTask();
SendSemaphore.Release();
}
public async Task<Tuple<int, PacketStream>> GetNextPacket(bool handleCompress, CancellationToken cancellationToken = default)
{
// ConsoleIO.WriteLine("GetNextPacket");
if (packetStream != null)
{
await packetStream.DisposeAsync();
packetStream = null;
}
int readed = 0;
(int packetSize, _) = await ReceiveVarIntRaw(ReadStream, cancellationToken);
int packetID;
if (handleCompress && CompressionThreshold > 0)
{
(int sizeUncompressed, readed) = await ReceiveVarIntRaw(ReadStream, cancellationToken);
if (sizeUncompressed != 0)
{
ZlibBaseStream zlibBaseStream = new(AesStream ?? ReadStream, packetSize: packetSize - readed);
ZLibStream zlibStream = new(zlibBaseStream, CompressionMode.Decompress, leaveOpen: false);
zlibBaseStream.BufferSize = 16;
(packetID, readed) = await ReceiveVarIntRaw(zlibStream, cancellationToken);
zlibBaseStream.BufferSize = 512;
// ConsoleIO.WriteLine("packetID = " + packetID + ", readed = " + zlibBaseStream.packetReaded + ", size = " + packetSize + " -> " + sizeUncompressed);
packetStream = new(zlibStream, sizeUncompressed - readed, cancellationToken);
return new(packetID, packetStream);
}
}
(packetID, int readed2) = await ReceiveVarIntRaw(ReadStream, cancellationToken);
packetStream = new(AesStream ?? ReadStream, packetSize - readed - readed2, cancellationToken);
return new(packetID, packetStream);
}
private async Task<Tuple<int, int>> ReceiveVarIntRaw(Stream stream, CancellationToken cancellationToken = default)
{
int i = 0;
int j = 0;
byte[] b = new byte[1];
while (true)
{
await stream.ReadAsync(b);
i |= (b[0] & 0x7F) << j++ * 7;
if (j > 5) throw new OverflowException("VarInt too big");
if ((b[0] & 0x80) != 128) break;
}
return new(i, j);
}
/// <summary>
/// Disconnect from the server
/// </summary>
public void Disconnect()
{
try
{
tcpClient.Close();
}
catch (SocketException) { }
catch (IOException) { }
catch (NullReferenceException) { }
catch (ObjectDisposedException) { }
}
}
}

View file

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using static ConsoleInteractive.ConsoleReader;
namespace MinecraftClient.Protocol.PacketPipeline
{
internal class ZlibBaseStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public int BufferSize { get; set; } = 16;
public int packetSize = 0, packetReaded = 0;
private Stream baseStream;
private AesStream? aesStream;
public ZlibBaseStream(Stream baseStream, int packetSize)
{
packetReaded = 0;
this.packetSize = packetSize;
this.baseStream = baseStream;
aesStream = null;
}
public ZlibBaseStream(AesStream aesStream, int packetSize)
{
packetReaded = 0;
this.packetSize = packetSize;
baseStream = this.aesStream = aesStream;
}
public override void Flush()
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
if (packetReaded == packetSize)
return 0;
int readed = baseStream.Read(buffer, offset, Math.Min(BufferSize, Math.Min(count, packetSize - packetReaded)));
packetReaded += readed;
return readed;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
int readLen = Math.Min(BufferSize, Math.Min(buffer.Length, packetSize - packetReaded));
if (packetReaded + readLen > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(buffer[..readLen], cancellationToken);
packetReaded += readLen;
return readLen;
}
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(buffer, cancellationToken);
packetReaded += buffer.Length;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public async Task Skip(int length)
{
if (aesStream != null)
{
int skipRaw = length - AesStream.BlockSize;
for (int readed = 0, curRead; readed < skipRaw; readed += curRead)
curRead = await aesStream.ReadRawAsync(PacketStream.DropBuf[..Math.Min(PacketStream.DropBufSize, skipRaw - readed)]);
await aesStream.ReadAsync(PacketStream.DropBuf[..Math.Min(length, AesStream.BlockSize)]);
}
else
{
for (int readed = 0, curRead; readed < length; readed += curRead)
curRead = await baseStream.ReadAsync(PacketStream.DropBuf[..Math.Min(PacketStream.DropBufSize, length - readed)]);
}
packetReaded += length;
}
public override async ValueTask DisposeAsync()
{
if (packetSize - packetReaded > 0)
{
// ConsoleIO.WriteLine("Zlib readed " + packetReaded + ", last " + (packetSize - packetReaded));
await Skip(packetSize - packetReaded);
}
}
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.ProfileKey
@ -10,51 +11,6 @@ namespace MinecraftClient.Protocol.ProfileKey
{
private static readonly SHA256 sha256Hash = SHA256.Create();
private static readonly string certificates = "https://api.minecraftservices.com/player/certificates";
public static PlayerKeyPair? GetNewProfileKeys(string accessToken)
{
ProxiedWebRequest.Response? response = null;
try
{
var request = new ProxiedWebRequest(certificates)
{
Accept = "application/json"
};
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
response = request.Post("application/json", "");
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.Body.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue,
sig: json.Properties["publicKeySignature"].StringValue,
sigV2: json.Properties["publicKeySignatureV2"].StringValue);
PrivateKey privateKey = new(pemKey: json.Properties["keyPair"].Properties["privateKey"].StringValue);
return new PlayerKeyPair(publicKey, privateKey,
expiresAt: json.Properties["expiresAt"].StringValue,
refreshedAfter: json.Properties["refreshedAfter"].StringValue);
}
catch (Exception e)
{
int code = response == null ? 0 : response.StatusCode;
ConsoleIO.WriteLineFormatted("§cFetch profile key failed: HttpCode = " + code + ", Error = " + e.Message);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
}
return null;
}
}
public static byte[] DecodePemKey(string key, string prefix, string suffix)
{
int i = key.IndexOf(prefix);

View file

@ -1,199 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Timers;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
namespace MinecraftClient.Protocol.ProfileKey
{
/// <summary>
/// Handle keys caching and storage.
/// </summary>
public static class KeysCache
{
private const string KeysCacheFilePlaintext = "ProfileKeyCache.ini";
private static FileMonitor? cachemonitor;
private static readonly Dictionary<string, PlayerKeyPair> keys = new();
private static readonly Timer updatetimer = new(100);
private static readonly List<KeyValuePair<string, PlayerKeyPair>> pendingadds = new();
private static readonly BinaryFormatter formatter = new();
/// <summary>
/// Retrieve whether KeysCache contains a keys for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>TRUE if keys are available</returns>
public static bool Contains(string login)
{
return keys.ContainsKey(login);
}
/// <summary>
/// Store keys and save it to disk if required.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="playerKeyPair">User keys</param>
public static void Store(string login, PlayerKeyPair playerKeyPair)
{
if (Contains(login))
{
keys[login] = playerKeyPair;
}
else
{
keys.Add(login, playerKeyPair);
}
if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk && updatetimer.Enabled == true)
{
pendingadds.Add(new KeyValuePair<string, PlayerKeyPair>(login, playerKeyPair));
}
else if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk)
{
SaveToDisk();
}
}
/// <summary>
/// Retrieve keys for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>PlayerKeyPair for given login</returns>
public static PlayerKeyPair Get(string login)
{
return keys[login];
}
/// <summary>
/// Initialize cache monitoring to keep cache updated with external changes.
/// </summary>
/// <returns>TRUE if keys are seeded from file</returns>
public static bool InitializeDiskCache()
{
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, KeysCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
updatetimer.Elapsed += HandlePending;
return LoadFromDisk();
}
/// <summary>
/// Reloads cache on external cache file change.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void OnChanged(object sender, FileSystemEventArgs e)
{
updatetimer.Stop();
updatetimer.Start();
}
/// <summary>
/// Called after timer elapsed. Reads disk cache and adds new/modified keys back.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void HandlePending(object? sender, ElapsedEventArgs e)
{
updatetimer.Stop();
LoadFromDisk();
foreach (KeyValuePair<string, PlayerKeyPair> pending in pendingadds.ToArray())
{
Store(pending.Key, pending.Value);
pendingadds.Remove(pending);
}
}
/// <summary>
/// Reads cache file and loads KeysInfos into KeysCache.
/// </summary>
/// <returns>True if data is successfully loaded</returns>
private static bool LoadFromDisk()
{
//User-editable keys cache file in text format
if (File.Exists(KeysCacheFilePlaintext))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_keys, KeysCacheFilePlaintext));
try
{
foreach (string line in FileMonitor.ReadAllLinesWithRetries(KeysCacheFilePlaintext))
{
if (!line.TrimStart().StartsWith("#"))
{
int separatorIdx = line.IndexOf('=');
if (separatorIdx >= 1 && line.Length > separatorIdx + 1)
{
string login = line[..separatorIdx];
string value = line[(separatorIdx + 1)..];
try
{
PlayerKeyPair playerKeyPair = PlayerKeyPair.FromString(value);
keys[login] = playerKeyPair;
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded_keys, playerKeyPair.ExpiresAt.ToString()));
}
catch (InvalidDataException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
}
catch (FormatException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
}
catch (ArgumentNullException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
}
}
else if (Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line_keys, line));
}
}
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain_keys, e.Message));
}
}
return keys.Count > 0;
}
/// <summary>
/// Saves player's keypair from KeysCache into cache file.
/// </summary>
private static void SaveToDisk()
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving_keys, acceptnewlines: true);
List<string> KeysCacheLines = new()
{
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
"# ProfileKey=PublicKey(base64),PublicKeySignature(base64),PublicKeySignatureV2(base64),PrivateKey(base64),ExpiresAt,RefreshAfter"
};
foreach (KeyValuePair<string, PlayerKeyPair> entry in keys)
KeysCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
try
{
FileMonitor.WriteAllLinesWithRetries(KeysCacheFilePlaintext, KeysCacheLines);
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail_keys, e.Message));
}
}
}
}

View file

@ -1,35 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
namespace MinecraftClient.Protocol.ProfileKey
{
public class PlayerKeyPair
{
[JsonInclude]
[JsonPropertyName("PublicKey")]
public PublicKey PublicKey;
[JsonInclude]
[JsonPropertyName("PrivateKey")]
public PrivateKey PrivateKey;
[JsonInclude]
[JsonPropertyName("ExpiresAt")]
public DateTime ExpiresAt;
public DateTime RefreshedAfter; // Todo: add a timer
[JsonInclude]
[JsonPropertyName("RefreshedAfter")]
public DateTime RefreshedAfter;
[JsonIgnore]
private const string DataTimeFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
public PlayerKeyPair(PublicKey keyPublic, PrivateKey keyPrivate, string expiresAt, string refreshedAfter)
[JsonConstructor]
public PlayerKeyPair(PublicKey PublicKey, PrivateKey PrivateKey, DateTime ExpiresAt, DateTime RefreshedAfter)
{
PublicKey = keyPublic;
PrivateKey = keyPrivate;
try
{
ExpiresAt = DateTime.ParseExact(expiresAt, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
RefreshedAfter = DateTime.ParseExact(refreshedAfter, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
}
catch
{
ExpiresAt = DateTime.Parse(expiresAt).ToUniversalTime();
RefreshedAfter = DateTime.Parse(refreshedAfter).ToUniversalTime();
}
this.PublicKey = PublicKey;
this.PrivateKey = PrivateKey;
this.ExpiresAt = ExpiresAt;
this.RefreshedAfter = RefreshedAfter;
}
public bool NeedRefresh()
@ -54,21 +57,6 @@ namespace MinecraftClient.Protocol.ProfileKey
return timeOffset.ToUnixTimeSeconds();
}
public static PlayerKeyPair FromString(string tokenString)
{
string[] fields = tokenString.Split(',');
if (fields.Length < 6)
throw new InvalidDataException("Invalid string format");
PublicKey publicKey = new(pemKey: fields[0].Trim(),
sig: fields[1].Trim(), sigV2: fields[2].Trim());
PrivateKey privateKey = new(pemKey: fields[3].Trim());
return new PlayerKeyPair(publicKey, privateKey, fields[4].Trim(), fields[5].Trim());
}
public override string ToString()
{
List<string> datas = new();

View file

@ -1,15 +1,20 @@
using System;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.ProfileKey
{
public class PrivateKey
{
[JsonInclude]
[JsonPropertyName("Key")]
public byte[] Key { get; set; }
[JsonIgnore]
private readonly RSA rsa;
[JsonIgnore]
private byte[]? precedingSignature = null;
public PrivateKey(string pemKey)
@ -20,6 +25,14 @@ namespace MinecraftClient.Protocol.ProfileKey
rsa.ImportPkcs8PrivateKey(Key, out _);
}
[JsonConstructor]
public PrivateKey(byte[] Key)
{
this.Key = Key;
rsa = RSA.Create();
rsa.ImportPkcs8PrivateKey(Key, out _);
}
public byte[] SignData(byte[] data)
{
return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

View file

@ -1,15 +1,26 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.ProfileKey
{
public class PublicKey
{
[JsonInclude]
[JsonPropertyName("Key")]
public byte[] Key { get; set; }
[JsonInclude]
[JsonPropertyName("Signature")]
public byte[]? Signature { get; set; }
[JsonInclude]
[JsonPropertyName("SignatureV2")]
public byte[]? SignatureV2 { get; set; }
[JsonIgnore]
private readonly RSA rsa;
public PublicKey(string pemKey, string? sig = null, string? sigV2 = null)
@ -36,6 +47,12 @@ namespace MinecraftClient.Protocol.ProfileKey
Signature = signature;
}
[JsonConstructor]
public PublicKey(byte[] Key, byte[]? Signature, byte[]? SignatureV2) : this(Key, Signature!)
{
this.SignatureV2 = SignatureV2;
}
public bool VerifyData(byte[] data, byte[] signature)
{
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Proxy;
namespace MinecraftClient.Protocol
@ -25,7 +26,7 @@ namespace MinecraftClient.Protocol
private readonly string httpVersion = "HTTP/1.1";
private ITcpFactory? tcpFactory;
private readonly ITcpFactory? tcpFactory;
private bool isProxied = false; // Send absolute Url in request if true
private readonly Uri uri;
@ -45,7 +46,7 @@ namespace MinecraftClient.Protocol
/// Set to true to tell the http client proxy is enabled
/// </summary>
public bool IsProxy { get { return isProxied; } set { isProxied = value; } }
public bool Debug { get { return Settings.Config.Logging.DebugMessages; } }
public static bool Debug { get { return Settings.Config.Logging.DebugMessages; } }
/// <summary>
/// Create a new http request
@ -105,9 +106,9 @@ namespace MinecraftClient.Protocol
/// Perform GET request and get the response. Proxy is handled automatically
/// </summary>
/// <returns></returns>
public Response Get()
public async Task<Response> Get()
{
return Send("GET");
return await Send("GET");
}
/// <summary>
@ -116,12 +117,12 @@ namespace MinecraftClient.Protocol
/// <param name="contentType">The content type of request body</param>
/// <param name="body">Request body</param>
/// <returns></returns>
public Response Post(string contentType, string body)
public async Task<Response> Post(string contentType, string body)
{
Headers.Add("Content-Type", contentType);
// Calculate length
Headers.Add("Content-Length", Encoding.UTF8.GetBytes(body).Length.ToString());
return Send("POST", body);
return await Send("POST", body);
}
/// <summary>
@ -130,35 +131,35 @@ namespace MinecraftClient.Protocol
/// <param name="method">Method in string representation</param>
/// <param name="body">Optional request body</param>
/// <returns></returns>
private Response Send(string method, string body = "")
private async Task<Response> Send(string method, string body = "")
{
List<string> requestMessage = new()
{
string.Format("{0} {1} {2}", method.ToUpper(), isProxied ? AbsoluteUrl : Path, httpVersion) // Request line
};
foreach (string key in Headers) // Headers
{
var value = Headers[key];
requestMessage.Add(string.Format("{0}: {1}", key, value));
}
requestMessage.Add(""); // <CR><LF>
if (body != "")
{
requestMessage.Add(body);
}
else requestMessage.Add(""); // <CR><LF>
else
requestMessage.Add(""); // <CR><LF>
if (Debug)
{
foreach (string l in requestMessage)
{
ConsoleIO.WriteLine("< " + l);
}
}
Response response = Response.Empty();
// FIXME: Use TcpFactory interface to avoid direct usage of the ProxyHandler class
// TcpClient client = tcpFactory.CreateTcpClient(Host, Port);
TcpClient client = ProxyHandler.NewTcpClient(Host, Port, true);
TcpClient client = ProxyHandler.NewTcpClient(Host, Port, ProxyHandler.ClientType.Login);
Stream stream;
if (IsSecure)
{
@ -171,35 +172,25 @@ namespace MinecraftClient.Protocol
}
string h = string.Join("\r\n", requestMessage.ToArray());
byte[] data = Encoding.ASCII.GetBytes(h);
stream.Write(data, 0, data.Length);
stream.Flush();
await stream.WriteAsync(data);
await stream.FlushAsync();
// Read response
int statusCode = ReadHttpStatus(stream);
var headers = ReadHeader(stream);
string? rbody;
if (headers.Get("transfer-encoding") == "chunked")
{
rbody = ReadBodyChunked(stream);
}
else
{
rbody = ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0"));
}
int statusCode = await ReadHttpStatus(stream);
var headers = await ReadHeader(stream);
Task<string> rbody = (headers.Get("transfer-encoding") == "chunked") ?
ReadBodyChunked(stream) : ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0"));
if (headers.Get("set-cookie") != null)
{
response.Cookies = ParseSetCookie(headers.GetValues("set-cookie") ?? Array.Empty<string>());
}
response.Body = rbody ?? "";
response.StatusCode = statusCode;
response.Headers = headers;
response.Body = await rbody;
try
{
stream.Close();
client.Close();
}
catch { }
try { stream.Close(); } catch { }
try { client.Close(); } catch { }
return response;
}
@ -210,9 +201,9 @@ namespace MinecraftClient.Protocol
/// <param name="s">Stream to read</param>
/// <returns></returns>
/// <exception cref="InvalidDataException">If server return unknown data</exception>
private static int ReadHttpStatus(Stream s)
private static async Task<int> ReadHttpStatus(Stream s)
{
var httpHeader = ReadLine(s); // http header line
var httpHeader = await ReadLine(s); // http header line
if (httpHeader.StartsWith("HTTP/1.1") || httpHeader.StartsWith("HTTP/1.0"))
{
return int.Parse(httpHeader.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture);
@ -228,15 +219,15 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="s">Stream to read</param>
/// <returns>Headers in lower-case</returns>
private static NameValueCollection ReadHeader(Stream s)
private static async Task<NameValueCollection> ReadHeader(Stream s)
{
var headers = new NameValueCollection();
// Read headers
string header;
do
{
header = ReadLine(s);
if (!String.IsNullOrEmpty(header))
header = await ReadLine(s);
if (!string.IsNullOrEmpty(header))
{
var tmp = header.Split(new char[] { ':' }, 2);
var name = tmp[0].ToLower();
@ -244,7 +235,7 @@ namespace MinecraftClient.Protocol
headers.Add(name, value);
}
}
while (!String.IsNullOrEmpty(header));
while (!string.IsNullOrEmpty(header));
return headers;
}
@ -254,23 +245,19 @@ namespace MinecraftClient.Protocol
/// <param name="s">Stream to read</param>
/// <param name="length">Length of the body (the Content-Length header)</param>
/// <returns>Body or null if length is zero</returns>
private static string? ReadBody(Stream s, int length)
private static async Task<string> ReadBody(Stream s, int length)
{
if (length > 0)
{
byte[] buffer = new byte[length];
int r = 0;
while (r < length)
{
var read = s.Read(buffer, r, length - r);
r += read;
Thread.Sleep(50);
}
int readed = 0;
while (readed < length)
readed += await s.ReadAsync(buffer.AsMemory(readed, length - readed));
return Encoding.UTF8.GetString(buffer);
}
else
{
return null;
return string.Empty;
}
}
@ -279,13 +266,13 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="s">Stream to read</param>
/// <returns>Body or empty string if nothing is received</returns>
private static string ReadBodyChunked(Stream s)
private static async Task<string> ReadBodyChunked(Stream s)
{
List<byte> buffer1 = new();
while (true)
{
string l = ReadLine(s);
int size = Int32.Parse(l, NumberStyles.HexNumber);
string l = await ReadLine(s);
int size = int.Parse(l, NumberStyles.HexNumber);
if (size == 0)
break;
byte[] buffer2 = new byte[size];
@ -296,7 +283,7 @@ namespace MinecraftClient.Protocol
r += read;
Thread.Sleep(50);
}
ReadLine(s);
await ReadLine(s);
buffer1.AddRange(buffer2);
}
return Encoding.UTF8.GetString(buffer1.ToArray());
@ -366,17 +353,15 @@ namespace MinecraftClient.Protocol
/// </remarks>
/// <param name="s">Stream to read</param>
/// <returns>String</returns>
private static string ReadLine(Stream s)
private static async Task<string> ReadLine(Stream s)
{
List<byte> buffer = new();
byte c;
byte[] c = new byte[1];
while (true)
{
int b = s.ReadByte();
if (b == -1)
break;
c = (byte)b;
if (c == '\n')
try { await s.ReadExactlyAsync(c, 0, 1); }
catch { break; }
if (c[0] == '\n')
{
if (buffer.Last() == '\r')
{
@ -384,7 +369,7 @@ namespace MinecraftClient.Protocol
break;
}
}
buffer.Add(c);
buffer.Add(c[0]);
}
return Encoding.UTF8.GetString(buffer.ToArray());
}

View file

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Ionic.Zip;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.PacketPalettes;
@ -135,15 +135,19 @@ namespace MinecraftClient.Protocol
MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
MetaData.SaveToFile();
using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open))
using (FileStream zipToOpen = new(Path.Combine(ReplayFileDirectory, replayFileName), FileMode.Open))
{
using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create);
using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open))
{
ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName);
recordingFile.CopyTo(recordingTmpFileEntry.Open());
}
using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open);
using ZipOutputStream zs = new(Path.Combine(ReplayFileDirectory, replayFileName));
zs.PutNextEntry(recordingTmpFileName);
recordingFile.CopyTo(zs);
zs.PutNextEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(zs);
zs.Close();
ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(metaDataFileEntry.Open());
}
File.Delete(Path.Combine(temporaryCache, recordingTmpFileName));
@ -165,20 +169,21 @@ namespace MinecraftClient.Protocol
MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
MetaData.SaveToFile();
using (Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open))
using (FileStream zipToOpen = new(replayFileName, FileMode.OpenOrCreate))
{
using ZipOutputStream zs = new(replayFileName);
zs.PutNextEntry(recordingTmpFileName);
using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create);
ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName);
// .CopyTo() method start from stream current position
// We need to reset position in order to get full content
var lastPosition = recordStream!.BaseStream.Position;
recordStream.BaseStream.Position = 0;
recordStream.BaseStream.CopyTo(zs);
recordStream.BaseStream.CopyTo(recordingTmpFileEntry.Open());
recordStream.BaseStream.Position = lastPosition;
zs.PutNextEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(zs);
zs.Close();
using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open);
ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(metaDataFileEntry.Open());
}
WriteDebugLog("Backup replay file created.");

View file

@ -1,9 +1,19 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.ServiceModel.Channels;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Timers;
using MinecraftClient.Protocol.ProfileKey;
using MinecraftClient.Scripting;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
@ -12,58 +22,88 @@ namespace MinecraftClient.Protocol.Session
/// <summary>
/// Handle sessions caching and storage.
/// </summary>
public static class SessionCache
public static partial class SessionCache
{
private const string SessionCacheFilePlaintext = "SessionCache.ini";
private const string SessionCacheFileSerialized = "SessionCache.db";
private static readonly string SessionCacheFileMinecraft = String.Concat(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Path.DirectorySeparatorChar,
".minecraft",
Path.DirectorySeparatorChar,
"launcher_profiles.json"
);
public class Cache
{
[JsonInclude]
public Dictionary<string, SessionToken> SessionTokens = new();
private static FileMonitor? cachemonitor;
private static readonly Dictionary<string, SessionToken> sessions = new();
private static readonly Timer updatetimer = new(100);
private static readonly List<KeyValuePair<string, SessionToken>> pendingadds = new();
private static readonly BinaryFormatter formatter = new();
[JsonInclude]
public Dictionary<string, PlayerKeyPair> ProfileKeys = new();
[JsonInclude]
public Dictionary<string, ServerInfo> ServerKeys = new();
public record ServerInfo
{
public ServerInfo(string serverIDhash, byte[] serverPublicKey)
{
ServerIDhash = serverIDhash;
ServerPublicKey = serverPublicKey;
}
public string? ServerIDhash { init; get; }
public byte[]? ServerPublicKey { init; get; }
}
}
private static Cache cache = new();
private const string SessionCacheFileJson = "SessionCache.json";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
{
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = false,
ReadCommentHandling = JsonCommentHandling.Skip,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
public static async Task ReadCacheSessionAsync()
{
if (File.Exists(SessionCacheFileJson))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFileJson));
FileStream fileStream = File.OpenRead(SessionCacheFileJson);
try
{
Cache? diskCache = (Cache?)await JsonSerializer.DeserializeAsync(fileStream, typeof(Cache), JsonOptions);
if (diskCache != null)
{
cache = diskCache;
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, cache.SessionTokens.Count, cache.ProfileKeys.Count));
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
catch (JsonException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
await fileStream.DisposeAsync();
}
}
/// <summary>
/// Retrieve whether SessionCache contains a session for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>TRUE if session is available</returns>
public static bool Contains(string login)
public static bool ContainsSession(string login)
{
return sessions.ContainsKey(login);
}
/// <summary>
/// Store a session and save it to disk if required.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="session">User session token used with Minecraft.net</param>
public static void Store(string login, SessionToken session)
{
if (Contains(login))
{
sessions[login] = session;
}
else
{
sessions.Add(login, session);
}
if (Config.Main.Advanced.SessionCache == CacheType.disk && updatetimer.Enabled == true)
{
pendingadds.Add(new KeyValuePair<string, SessionToken>(login, session));
}
else if (Config.Main.Advanced.SessionCache == CacheType.disk)
{
SaveToDisk();
}
return cache.SessionTokens.ContainsKey(login);
}
/// <summary>
@ -71,202 +111,92 @@ namespace MinecraftClient.Protocol.Session
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>SessionToken for given login</returns>
public static SessionToken Get(string login)
public static Tuple<SessionToken?, PlayerKeyPair?> GetSession(string login)
{
return sessions[login];
cache.SessionTokens.TryGetValue(login, out SessionToken? sessionToken);
cache.ProfileKeys.TryGetValue(login, out PlayerKeyPair? playerKeyPair);
return new(sessionToken, playerKeyPair);
}
public static Cache.ServerInfo? GetServerInfo(string server)
{
if (cache.ServerKeys.TryGetValue(server, out Cache.ServerInfo? info))
return info;
else
return null;
}
/// <summary>
/// Initialize cache monitoring to keep cache updated with external changes.
/// Store a session and save it to disk if required.
/// </summary>
/// <returns>TRUE if session tokens are seeded from file</returns>
public static bool InitializeDiskCache()
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="newSession">User session token used with Minecraft.net</param>
public static async Task StoreSessionAsync(string login, SessionToken? sessionToken, PlayerKeyPair? profileKey)
{
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
updatetimer.Elapsed += HandlePending;
return LoadFromDisk();
if (sessionToken != null)
cache.SessionTokens[login] = sessionToken;
if (profileKey != null)
cache.ProfileKeys[login] = profileKey;
if (Config.Main.Advanced.SessionCache == CacheType.disk)
await SaveToDisk();
}
/// <summary>
/// Reloads cache on external cache file change.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void OnChanged(object sender, FileSystemEventArgs e)
public static void StoreServerInfo(string server, string ServerIDhash, byte[] ServerPublicKey)
{
updatetimer.Stop();
updatetimer.Start();
}
/// <summary>
/// Called after timer elapsed. Reads disk cache and adds new/modified sessions back.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void HandlePending(object? sender, ElapsedEventArgs e)
{
updatetimer.Stop();
LoadFromDisk();
foreach (KeyValuePair<string, SessionToken> pending in pendingadds.ToArray())
{
Store(pending.Key, pending.Value);
pendingadds.Remove(pending);
}
}
/// <summary>
/// Reads cache file and loads SessionTokens into SessionCache.
/// </summary>
/// <returns>True if data is successfully loaded</returns>
private static bool LoadFromDisk()
{
//Grab sessions in the Minecraft directory
if (File.Exists(SessionCacheFileMinecraft))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft)));
Json.JSONData mcSession = new(Json.JSONData.DataType.String);
try
{
mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft));
}
catch (IOException) { /* Failed to read file from disk -- ignoring */ }
if (mcSession.Type == Json.JSONData.DataType.Object
&& mcSession.Properties.ContainsKey("clientToken")
&& mcSession.Properties.ContainsKey("authenticationDatabase"))
{
string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", "");
Dictionary<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
foreach (string key in sessionItems.Keys)
{
if (Guid.TryParseExact(key, "N", out Guid temp))
{
Dictionary<string, Json.JSONData> sessionItem = sessionItems[key].Properties;
if (sessionItem.ContainsKey("displayName")
&& sessionItem.ContainsKey("accessToken")
&& sessionItem.ContainsKey("username")
&& sessionItem.ContainsKey("uuid"))
{
string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue);
try
{
SessionToken session = SessionToken.FromString(String.Join(",",
sessionItem["accessToken"].StringValue,
sessionItem["displayName"].StringValue,
sessionItem["uuid"].StringValue.Replace("-", ""),
clientID
));
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException) { /* Not a valid session */ }
}
}
}
}
}
//Serialized session cache file in binary format
if (File.Exists(SessionCacheFileSerialized))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized));
try
{
using FileStream fs = new(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read);
#pragma warning disable SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
// Possible risk of information disclosure or remote code execution. The impact of this vulnerability is limited to the user side only.
Dictionary<string, SessionToken> sessionsTemp = (Dictionary<string, SessionToken>)formatter.Deserialize(fs);
#pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
foreach (KeyValuePair<string, SessionToken> item in sessionsTemp)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID));
sessions[item.Key] = item.Value;
}
}
catch (IOException ex)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message));
}
catch (SerializationException ex2)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message));
}
}
//User-editable session cache file in text format
if (File.Exists(SessionCacheFilePlaintext))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFilePlaintext));
try
{
foreach (string line in FileMonitor.ReadAllLinesWithRetries(SessionCacheFilePlaintext))
{
if (!line.Trim().StartsWith("#"))
{
string[] keyValue = line.Split('=');
if (keyValue.Length == 2)
{
try
{
string login = Settings.ToLowerIfNeed(keyValue[0]);
SessionToken session = SessionToken.FromString(keyValue[1]);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string, keyValue[1], e.Message));
}
}
else if (Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line, line));
}
}
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
}
return sessions.Count > 0;
cache.ServerKeys[server] = new(ServerIDhash, ServerPublicKey);
}
/// <summary>
/// Saves SessionToken's from SessionCache into cache file.
/// </summary>
private static void SaveToDisk()
private static async Task SaveToDisk()
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true);
List<string> sessionCacheLines = new()
foreach ((string login, SessionToken session) in cache.SessionTokens)
{
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
"# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey"
};
foreach (KeyValuePair<string, SessionToken> entry in sessions)
sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
if (!GetJwtRegex().IsMatch(session.ID))
cache.SessionTokens.Remove(login);
else if (!ChatBot.IsValidName(session.PlayerName))
cache.SessionTokens.Remove(login);
else if (!Guid.TryParseExact(session.PlayerID, "N", out _))
cache.SessionTokens.Remove(login);
else if (!Guid.TryParseExact(session.ClientID, "N", out _))
cache.SessionTokens.Remove(login);
// No validation on refresh token because it is custom format token (not Jwt)
}
foreach ((string login, PlayerKeyPair profileKey) in cache.ProfileKeys)
{
if (profileKey.NeedRefresh())
cache.ProfileKeys.Remove(login);
}
try
{
FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines);
FileStream fileStream = File.Open(SessionCacheFileJson, FileMode.Create);
await fileStream.WriteAsync(Encoding.UTF8.GetBytes($"/* Generated by MCC v{Program.Version} - Keep it secret & Edit at own risk! */{Environment.NewLine}"));
await JsonSerializer.SerializeAsync(fileStream, cache, typeof(Cache), JsonOptions);
await fileStream.FlushAsync();
fileStream.Close();
await fileStream.DisposeAsync();
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
}
catch (JsonException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
}
}
[GeneratedRegex("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$", RegexOptions.Compiled)]
private static partial Regex GetJwtRegex();
}
}

View file

@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MinecraftClient.Scripting;
@ -9,92 +11,39 @@ namespace MinecraftClient.Protocol.Session
[Serializable]
public class SessionToken
{
private static readonly Regex JwtRegex = new("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$");
[JsonInclude]
[JsonPropertyName("SessionID")]
public string ID { get; set; }
public string PlayerName { get; set; }
public string PlayerID { get; set; }
public string ClientID { get; set; }
public string RefreshToken { get; set; }
public string ServerIDhash { get; set; }
public byte[]? ServerPublicKey { get; set; }
[JsonInclude]
[JsonPropertyName("PlayerName")]
public string PlayerName { get; set; }
[JsonInclude]
[JsonPropertyName("PlayerID")]
public string PlayerID { get; set; }
[JsonInclude]
[JsonPropertyName("ClientID")]
public string ClientID { get; set; }
[JsonInclude]
[JsonPropertyName("RefreshToken")]
public string RefreshToken { get; set; }
[JsonIgnore]
public string? ServerInfoHash = null;
[JsonIgnore]
public Task<bool>? SessionPreCheckTask = null;
public SessionToken()
{
ID = String.Empty;
PlayerName = String.Empty;
PlayerID = String.Empty;
ClientID = String.Empty;
RefreshToken = String.Empty;
ServerIDhash = String.Empty;
ServerPublicKey = null;
}
public bool SessionPreCheck()
{
if (ID == string.Empty || PlayerID == String.Empty || ServerPublicKey == null)
return false;
Crypto.CryptoHandler.ClientAESPrivateKey ??= Crypto.CryptoHandler.GenerateAESPrivateKey();
string serverHash = Crypto.CryptoHandler.GetServerHash(ServerIDhash, ServerPublicKey, Crypto.CryptoHandler.ClientAESPrivateKey);
if (ProtocolHandler.SessionCheck(PlayerID, ID, serverHash))
return true;
return false;
}
public override string ToString()
{
return String.Join(",", ID, PlayerName, PlayerID, ClientID, RefreshToken, ServerIDhash,
(ServerPublicKey == null) ? String.Empty : Convert.ToBase64String(ServerPublicKey));
}
public static SessionToken FromString(string tokenString)
{
string[] fields = tokenString.Split(',');
if (fields.Length < 4)
throw new InvalidDataException("Invalid string format");
SessionToken session = new()
{
ID = fields[0],
PlayerName = fields[1],
PlayerID = fields[2],
ClientID = fields[3]
};
// Backward compatible with old session file without refresh token field
if (fields.Length > 4)
session.RefreshToken = fields[4];
else
session.RefreshToken = String.Empty;
if (fields.Length > 5)
session.ServerIDhash = fields[5];
else
session.ServerIDhash = String.Empty;
if (fields.Length > 6)
{
try
{
session.ServerPublicKey = Convert.FromBase64String(fields[6]);
}
catch
{
session.ServerPublicKey = null;
}
}
else
session.ServerPublicKey = null;
if (!JwtRegex.IsMatch(session.ID))
throw new InvalidDataException("Invalid session ID");
if (!ChatBot.IsValidName(session.PlayerName))
throw new InvalidDataException("Invalid player name");
if (!Guid.TryParseExact(session.PlayerID, "N", out _))
throw new InvalidDataException("Invalid player ID");
if (!Guid.TryParseExact(session.ClientID, "N", out _))
throw new InvalidDataException("Invalid client ID");
// No validation on refresh token because it is custom format token (not Jwt)
return session;
ID = string.Empty;
PlayerName = string.Empty;
PlayerID = string.Empty;
ClientID = string.Empty;
RefreshToken = string.Empty;
}
}
}