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,