diff --git a/ConfigGUI/MainForm.Designer.cs b/ConfigGUI/MainForm.Designer.cs index a5d1e96..aca1b18 100644 --- a/ConfigGUI/MainForm.Designer.cs +++ b/ConfigGUI/MainForm.Designer.cs @@ -1115,7 +1115,7 @@ partial class MainForm { this.tDefaultTerrain.Name = "tDefaultTerrain"; this.tDefaultTerrain.Size = new System.Drawing.Size(547, 21); this.tDefaultTerrain.TabIndex = 24; - this.tDefaultTerrain.Text = "http://123DMWM.com/TexturePacks/64xDefault.zip"; + this.tDefaultTerrain.Text = "https://123DMWM.com/TexturePacks/64xDefault.zip"; // // lDefaultTerrain // diff --git a/fCraft/Commands/CpeCommands.cs b/fCraft/Commands/CpeCommands.cs index 3608681..649cdbf 100644 --- a/fCraft/Commands/CpeCommands.cs +++ b/fCraft/Commands/CpeCommands.cs @@ -1369,7 +1369,7 @@ static class CpeCommands { case "tex": case "texture": case "terrain": - p.Message("Terrain IDs: &9http://123DMWM.com/ID-Overlay.png"); + p.Message("Terrain IDs: &9https://123DMWM.com/ID-Overlay.png"); p.Message("Current world terrain: &9{0}", p.World.Texture.CaselessEquals("default") ? Server.DefaultTerrain : p.World.Texture); break; default: diff --git a/fCraft/Commands/MaintenanceCommands.cs b/fCraft/Commands/MaintenanceCommands.cs index 7172616..4d2d9bf 100644 --- a/fCraft/Commands/MaintenanceCommands.cs +++ b/fCraft/Commands/MaintenanceCommands.cs @@ -1367,14 +1367,14 @@ static void SaveHandler(Player player, CommandReader cmd) player.Message(" &7" + latest.ToLongDateString() + " &Sat &7" + latest.ToLongTimeString()); } catch (Exception ex) { Logger.Log(LogType.Error, "Updates.UpdaterHandler:" + ex); - player.Message("Cannot access http://123DMWM.com/ at the moment."); + player.Message("Cannot access https://123DMWM.com/ at the moment."); } TimeSpan currentDelta = DateTime.UtcNow - current; player.Message("Server file last update (&7" + currentDelta.ToMiniString() + " &Sago):"); player.Message(" &7" + current.ToLongDateString() + " &Sat &7" + current.ToLongTimeString()); - player.Message("Download updated Zip here: &9http://123DMWM.com/ProCraft/Builds/Latest.zip"); + player.Message("Download updated Zip here: &9https://123DMWM.com/ProCraft/Builds/Latest.zip"); } #endregion #region AutoRankCheck diff --git a/fCraft/Commands/WorldCommands.cs b/fCraft/Commands/WorldCommands.cs index d914459..2ba78a3 100644 --- a/fCraft/Commands/WorldCommands.cs +++ b/fCraft/Commands/WorldCommands.cs @@ -1139,7 +1139,7 @@ class BlockDBCounterProcessor : IBlockDBQueryProcessor { if (urlString.StartsWith("++")) urlString = "http://i.imgur.com/" + urlString.Substring(2) + ".png"; if (!urlString.CaselessStarts("http://") && !urlString.CaselessStarts("https://")) urlString = "http://" + urlString; - if (!urlString.CaselessStarts("http://i.imgur.com/") && !urlString.CaselessStarts("http://123DMWM.com/")) { + if (!urlString.CaselessStarts("http://i.imgur.com/") && !urlString.CaselessStarts("https://123DMWM.com/")) { player.Message("For safety reasons we only accept images uploaded to &9http://imgur.com/ &SSorry for this inconvenience."); player.Message(" You cannot use: &9" + urlString); return false; diff --git a/fCraft/Network/Heartbeat.cs b/fCraft/Network/Heartbeat.cs index 2cfa87c..6720ac5 100644 --- a/fCraft/Network/Heartbeat.cs +++ b/fCraft/Network/Heartbeat.cs @@ -246,6 +246,7 @@ public sealed class HeartbeatData { internal HeartbeatData([NotNull] Uri heartbeatUri) { if (heartbeatUri == null) throw new ArgumentNullException("heartbeatUri"); IsPublic = ConfigKey.IsPublic.Enabled(); + IsWeb = ConfigKey.IsWeb.Enabled(); MaxPlayers = ConfigKey.MaxPlayers.GetInt(); PlayerCount = Server.CountPlayers(false); Port = Server.Port; @@ -286,6 +287,9 @@ public sealed class HeartbeatData { /// Whether or not the server should be listed on minecraft.net public bool IsPublic { get; set; } + /// Whether or not the server should allow Web Clients + public bool IsWeb { get; set; } + /// Version of the classic minecraft protocol that this server is using. public int ProtocolVersion { get; set; } @@ -298,7 +302,7 @@ public sealed class HeartbeatData { internal Uri CreateUri() { UriBuilder ub = new UriBuilder(HeartbeatUri); StringBuilder sb = new StringBuilder(); - sb.AppendFormat("public={0}&max={1}&users={2}&port={3}&version={4}&salt={5}&name={6}&software={7}", + sb.AppendFormat("public={0}&max={1}&users={2}&port={3}&version={4}&salt={5}&name={6}&software={7}&web={8}", IsPublic, MaxPlayers, PlayerCount, @@ -306,7 +310,8 @@ public sealed class HeartbeatData { ProtocolVersion, Uri.EscapeDataString(Salt), Uri.EscapeDataString(ServerName), - Server.Software.Replace("&", "%26")); + Server.Software.Replace("&", "%26"), + IsWeb); foreach (var pair in CustomData) { sb.AppendFormat("&{0}={1}", Uri.EscapeDataString(pair.Key), diff --git a/fCraft/Network/Player.Handshake.cs b/fCraft/Network/Player.Handshake.cs index cd8535e..7ff7182 100644 --- a/fCraft/Network/Player.Handshake.cs +++ b/fCraft/Network/Player.Handshake.cs @@ -30,7 +30,10 @@ public sealed partial class Player { return false; case (byte)'G': // WoM GET requests - return false; + stream = new WebSocketStream(stream); + reader = new PacketReader(stream); + writer = new PacketWriter(stream); + return reader.ReadByte() == (byte)OpCode.Handshake; default: if (CheckModernSMP(opcode)) return false; @@ -93,7 +96,7 @@ public sealed partial class Player { string favicon = ImageFromFavicon(); if (favicon != null) return favicon; - // Base64 encoding of http://123DMWM.com/I/299.png; + // Base64 encoding of https://123DMWM.com/I/299.png; return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsI" + "BFShKgAAAABl0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC4xMK0KCsAAAAoJSURBVHhe7ZoJcFVXHcYf27CWpZ2OzrS2Tkft1KpTR6u12mrV" + "Wu02ShdrVSwudFoHqZbF0BSEkLAGSMIOBcK+rwUKlFUoYMAGyjphCWEPi8O+c/x+592TuWYCPW8LCeN/5jdvybnnnO87/7Pc+xIxHhGJRCqdm" + diff --git a/fCraft/Network/Player.Networking.cs b/fCraft/Network/Player.Networking.cs index e413217..98adb2a 100644 --- a/fCraft/Network/Player.Networking.cs +++ b/fCraft/Network/Player.Networking.cs @@ -15,6 +15,7 @@ using fCraft.MapConversion; using JetBrains.Annotations; using System.Diagnostics; +using System.Security.Cryptography; namespace fCraft { /// Represents a connection to a Minecraft client. Handles low-level interactions (e.g. networking). @@ -43,13 +44,14 @@ public sealed partial class Player { bool canReceive = true, canSend = true, canQueue = true; + public bool IsWebSocket { get; set; } bool unregisterOnKick = true; readonly Thread ioThread; readonly TcpClient client; - readonly NetworkStream stream; - readonly PacketReader reader; - readonly PacketWriter writer; + HackyStream stream; + PacketReader reader; + PacketWriter writer; readonly ConcurrentQueue priorityOutputQueue = new ConcurrentQueue(); readonly ConcurrentQueue blockQueue = new ConcurrentQueue(); @@ -93,7 +95,8 @@ public sealed partial class Player { IP = ( (IPEndPoint)( client.Client.RemoteEndPoint ) ).Address; if( Server.RaiseSessionConnectingEvent( IP ) ) return; - stream = client.GetStream(); + NetworkStream netStream = client.GetStream(); + stream = new HackyNetStream(netStream); reader = new PacketReader( stream ); writer = new PacketWriter( stream ); @@ -258,7 +261,7 @@ public sealed partial class Player { // get input from player - while( canReceive && stream.DataAvailable ) { + while( canReceive && stream.HasDataAvailable) { byte opcode = reader.ReadByte(); switch( (OpCode)opcode ) { @@ -571,6 +574,266 @@ public sealed partial class Player { return client.CaselessContains( "ClassiCube" ) || client.CaselessContains( "ClassicalSharp" ); } + // NOTE: Because Stream doesn't have DataAvailable and we need this info + public abstract class HackyStream : Stream { + static NotSupportedException ex = new NotSupportedException("Unsupported I/O operation"); + public abstract bool HasDataAvailable { get; } + + public override bool CanSeek { get { return false; } } + public override void SetLength(long value) { throw ex; } + public override long Seek(long offset, SeekOrigin origin) { throw ex; } + + public override long Length { get { throw ex; } } + public override long Position { get { throw ex; } set { throw ex; } } + } + + public sealed class HackyNetStream : HackyStream { + NetworkStream underlying; + + public override bool CanRead { get { return underlying.CanRead; } } + public override bool CanWrite { get { return underlying.CanWrite; } } + public override void Close() { underlying.Close(); } + public override void Flush() { underlying.Flush(); } + public override bool HasDataAvailable { get { return underlying.DataAvailable; } } + + public HackyNetStream(NetworkStream underlying) { + this.underlying = underlying; + } + + public override int Read(byte[] buffer, int offset, int count) { + if (underlying.CanRead) { + return underlying.Read(buffer, offset, count); + } else { + return 0; + } + } + + public override void Write(byte[] buffer, int offset, int count) { + underlying.Write(buffer, offset, count); + } + } + + public sealed class WebSocketStream : HackyStream { + HackyStream underlying; + + public override bool CanRead { get { return true; } } + public override bool CanWrite { get { return true; } } + public override void Close() { underlying.Close(); } + public override void Flush() { underlying.Flush(); } + // TODO: Probably inaccurate + public override bool HasDataAvailable { get { return underlying.HasDataAvailable; } } + + public WebSocketStream(HackyStream underlying) { + this.underlying = underlying; + } + + bool readingHeaders = true; + bool conn, upgrade, version, proto; + string verKey; + + // NetworkStream.ReadByte allocates new byte[1] each time it's called + // So just allocate one array here instead + byte[] tmp = new byte[1]; + byte ReadRawByte() { + int len = underlying.Read(tmp, 0, 1); + if (len == 0) + throw new EndOfStreamException(); + return tmp[0]; + } + + void Disconnect(int reason) { + byte[] packet = new byte[4]; + packet[0] = 0x88; // FIN BIT, close opcode + packet[1] = 2; + packet[2] = (byte)(reason >> 8); + packet[3] = (byte)reason; + + underlying.Write(packet, 0, packet.Length); + underlying.Close(); + } + + void AcceptConnection() { + const string fmt = + "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: {0}\r\n" + + "Sec-WebSocket-Protocol: ClassiCube\r\n" + + "\r\n"; + + string key = verKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + SHA1 sha = SHA1.Create(); + byte[] raw = sha.ComputeHash(Encoding.ASCII.GetBytes(key)); + + string headers = String.Format(fmt, Convert.ToBase64String(raw)); + byte[] packet = Encoding.ASCII.GetBytes(headers); + underlying.Write(packet, 0, packet.Length); + readingHeaders = false; + } + + void ProcessHeader(string raw) { + // end of headers + if (raw.Length == 0) { + if (conn && upgrade && version && proto && verKey != null) { + //System.Console.WriteLine("OK!"); + AcceptConnection(); + } else { + //System.Console.WriteLine("NO DICE"); + // don't pretend to be a http server (so IP:port isn't marked as one by bots) + Close(); + } + } + + int sep = raw.IndexOf(':'); + if (sep == -1) + return; // not a proper header + string key = raw.Substring(0, sep); + string val = raw.Substring(sep + 1).Trim(); + + // TODO: debug + //System.Console.WriteLine(key + " : " + val); + + if (key == "Connection") { + conn = val.Contains("Upgrade"); + } else if (key == "Upgrade") { + upgrade = val == "websocket"; + } else if (key == "Sec-WebSocket-Version") { + version = val == "13"; + } else if (key == "Sec-WebSocket-Key") { + verKey = val; + } else if (key == "Sec-WebSocket-Protocol") { + proto = val == "ClassiCube"; + } + } + + void ReadHeader() { + // NOTE: probably vulnerable to attack heree + List raw = new List(); + + for (;;){ + byte next = ReadRawByte(); + if (next == '\r') + continue; + if (next != '\n') { raw.Add(next); continue; } + + string value = Encoding.ASCII.GetString(raw.ToArray()); + ProcessHeader(value); + raw.Clear(); + break; + } + } + + + byte[] mask = new byte[4], frame; + int frameLen, dataOffset, dataLength; + + void ReadFrame() { + int opcode = ReadRawByte() & 0x0F; + int flags = ReadRawByte() & 0x7F; + dataLength = 0; // reset packet data + + if (flags == 127) { + // unsupported 8 byte extended length + Disconnect(1009); + return; + } else if (flags == 126) { + // 2 byte length + frameLen = (ReadRawByte() << 8) | ReadRawByte(); + } else { + // length is inline + frameLen = flags; + } + + // read mask (always in client packets) + for (int i = 0;i < 4;i++) { + mask[i] = ReadRawByte(); + } + + // read frame data + if (frame == null || frameLen > frame.Length) + frame = new byte[frameLen]; + + for (int offset = 0, left = frameLen;left > 0;) { + int read = underlying.Read(frame, offset, left); + if (read == 0) + throw new EndOfStreamException(); + + offset += read; + left -= read; + } + + // decode frame data + for (int i = 0;i < frameLen;i++) { + frame[i] ^= mask[i & 3]; + } + + switch (opcode) { + // TODO: reply to ping frames + case 0x00: + case 0x02: + // normal frames + dataOffset = 0; + dataLength = frameLen; + break; + case 0x08: + // Connection is getting closed + Disconnect(1000); + break; + default: + Disconnect(1003); + break; + } + } + + public override int Read(byte[] buffer, int offset, int length) { + int read = 0; + while (readingHeaders) + ReadHeader(); + + while (length > 0) { + // read next frame (might not be a data frame though) + if (dataLength == 0) + ReadFrame(); + + // read next data frame data + if (dataLength > 0) { + int copy = Math.Min(length, dataLength); + Buffer.BlockCopy(frame, dataOffset, buffer, offset, copy); + + length -= copy; + offset += copy; + dataOffset += copy; + dataLength -= copy; + read += copy; + } + } + return read; + } + + + static byte[] WrapData(byte[] data, int offset, int length) { + int headerLen = 2 + (length >= 126 ? 2 : 0); + byte[] packet = new byte[headerLen + length]; + packet[0] = 0x82; // FIN bit, binary opcode + + if (headerLen > 2) { + packet[1] = 126; + packet[2] = (byte)(length >> 8); + packet[3] = (byte)length; + } else { + packet[1] = (byte)length; + } + Buffer.BlockCopy(data, offset, packet, headerLen, length); + return packet; + } + + public override void Write(byte[] buffer, int offset, int count) { + byte[] packet = WrapData(buffer, offset, count); + underlying.Write(packet, 0, packet.Length); + } + + } + bool LoginSequence() { byte opCode = reader.ReadByte(); @@ -797,8 +1060,8 @@ bool LoginSequence() Message("&bIt is recommended that you switch to Enhanced mode!"); Message("&bClick &aOptions &b-> &aMode &b-> &aEnhanced &bin the launcher."); } else if (!IsModernClient(ClientName)) { - Message("&bIt is recommended that you use the ClassicalSharp client!"); - Message("&9http://123DMWM.com/cs &bredirects to the official download."); + Message("&bIt is recommended that you use the new ClassiCube client!"); + Message("&bYou can download it here: &9http://www.classicube.net/download/"); } diff --git a/fCraft/System/ConfigKey.cs b/fCraft/System/ConfigKey.cs index 7ff556e..08a8d09 100644 --- a/fCraft/System/ConfigKey.cs +++ b/fCraft/System/ConfigKey.cs @@ -58,6 +58,11 @@ public enum ConfigKey { IsPublic, + [BoolKey(ConfigSection.General, true, + @"WebClient support allows users to connect to your server using the ClassiCube web client.")] + IsWeb, + + [IntKey( ConfigSection.General, 25565, @"Port number on your local machine that ProCraft uses to listen for incoming connections. If you are behind a router, you may need @@ -161,7 +166,7 @@ public enum ConfigKey { make sure to move the map files before starting the server again." )] MapPath, - [StringKey(ConfigSection.Worlds, "http://123DMWM.com/TexturePacks/64xDefault.zip", + [StringKey(ConfigSection.Worlds, "https://123DMWM.com/TexturePacks/64xDefault.zip", @"Custom URL for world terrain")] DefaultTerrain,