Skip to content

008: Reading a Packet

Shane DeSeranno edited this page Oct 10, 2017 · 5 revisions

We will start this section by creating a new class Packet. For better organization, I recommend creating a folder called Packets to hold this, and in C#, all of the packets will be in the Packets sub-namespace. The Packet class should be abstract, as we'll never create a simple packet and only inherited classes will have instances.

Also, we will want to add a helper class called ByteReader at the root of the project. This will help us read the bytes from the byte array. It is mostly a nice wrapper around a MemoryStream to deal with reading in network byte order:

    public class ByteReader : IDisposable
    {
        private readonly char[] ListSeparator = new char[] { ',' };
        private MemoryStream m_Stream;

        public bool IsEOF
        {
            get
            {
                if (disposedValue)
                    throw new ObjectDisposedException("ByteReader");

                return m_Stream.Position == m_Stream.Length;
            }
        }

        public ByteReader(byte[] data)
        {
            m_Stream = new MemoryStream(data);
        }

        public byte[] GetBytes(int length)
        {
            if (disposedValue)
                throw new ObjectDisposedException("ByteReader");

            byte[] data = new byte[length];
            m_Stream.Read(data, 0, length);
            return data;
        }

        public byte[] GetMPInt()
        {
            uint size = GetUInt32();

            if (size == 0)
                return new byte[1];

            byte[] data = GetBytes((int)size);
            if (data[0] == 0)
                return data.Skip(1).ToArray();

            return data;
        }

        public uint GetUInt32()
        {
            byte[] data = GetBytes(4);
            if (BitConverter.IsLittleEndian)
                data = data.Reverse().ToArray();
            return BitConverter.ToUInt32(data, 0);
        }

        public string GetString()
        {
            return GetString(Encoding.ASCII);
        }

        public string GetString(Encoding encoding)
        {
            int length = (int)GetUInt32();

            if (length == 0)
                return string.Empty;

            return encoding.GetString(GetBytes(length));
        }

        public List<string> GetNameList()
        {
            List<string> data = new List<string>();

            return new List<string>(GetString().Split(ListSeparator, StringSplitOptions.RemoveEmptyEntries));
        }

        public bool GetBoolean()
        {
            return (GetByte() != 0);
        }


        public byte GetByte()
        {
            if (disposedValue)
                throw new ObjectDisposedException("ByteReader");

            return (byte)m_Stream.ReadByte();
        }

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    m_Stream.Dispose();
                    m_Stream = null;
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
        }
        #endregion
    }

Then we can add a static method to the Packet class for reading a packet:

    public abstract class Packet
    {
        // https://tools.ietf.org/html/rfc4253#section-6.1
        public const int MaxPacketSize = 35000;

        private static int s_PacketHeaderSize = 5;

        public static Packet ReadPacket(Socket socket)
        {
            if (socket == null)
                return null;

            // TODO: Get the block size based on the ClientToServer cipher
            uint blockSize = 8;

            // We must have at least 1 block to read
            if (socket.Available < blockSize)
                return null;  // Packet not here

            byte[] firstBlock = new byte[blockSize];
            int bytesRead = socket.Receive(firstBlock);
            if (bytesRead != blockSize)
                throw new Exception("Failed to read from socket.");

            // TODO: Decrypt the block using the ClientToServer cipher

            uint packetLength = 0;
            byte paddingLength = 0;
            using (ByteReader reader = new ByteReader(firstBlock))
            {
                // uint32    packet_length
                // packet_length
                //     The length of the packet in bytes, not including 'mac' or the
                //     'packet_length' field itself.
                packetLength = reader.GetUInt32();
                if (packetLength > MaxPacketSize)
                    throw new Exception($"Client tried to send a packet bigger than MaxPacketSize ({MaxPacketSize} bytes): {packetLength} bytes");

                // byte      padding_length
                // padding_length
                //    Length of 'random padding' (bytes).
                paddingLength = reader.GetByte();
            }

            // byte[n1]  payload; n1 = packet_length - padding_length - 1
            // payload
            //    The useful contents of the packet.  If compression has been
            //    negotiated, this field is compressed.  Initially, compression
            //    MUST be "none".
            uint bytesToRead = packetLength - blockSize + 4;

            byte[] restOfPacket = new byte[bytesToRead];
            bytesRead = socket.Receive(restOfPacket);
            if (bytesRead != bytesToRead)
                throw new Exception("Failed to read from socket.");

            // TODO: Decrypt the blocks using the ClientToServer cipher

            uint payloadLength = packetLength - paddingLength - 1;
            byte[] fullPacket = firstBlock.Concat(restOfPacket).ToArray();

            // TODO: Track total bytes read

            byte[] payload = fullPacket.Skip(s_PacketHeaderSize).Take((int)(packetLength - paddingLength - 1)).ToArray();

            // byte[n2]  random padding; n2 = padding_length
            // random padding
            //    Arbitrary-length padding, such that the total length of
            //    (packet_length || padding_length || payload || random padding)
            //    is a multiple of the cipher block size or 8, whichever is
            //    larger.  There MUST be at least four bytes of padding.  The
            //    padding SHOULD consist of random bytes.  The maximum amount of
            //    padding is 255 bytes.

            // byte[m]   mac (Message Authentication Code - MAC); m = mac_length
            // mac
            //    Message Authentication Code.  If message authentication has
            //    been negotiated, this field contains the MAC bytes.  Initially,
            //    the MAC algorithm MUST be "none".

            // TODO: Keep track of the received packet sequence (used for MAC)

            // TODO: Read MAC if present

            // TODO: Decompress the payload if necessary

            using (ByteReader packetReader = new ByteReader(payload))
            {
                // TODO: Create a packet object and return it
            }

            return null;
        }
    }

And now, all we need to do is update our Client's Poll() method to process packets:

                if (m_HasCompletedProtocolVersionExchange)
                {
                    try
                    {
                        Packet packet = Packet.ReadPacket(m_Socket);
                        while (packet != null)
                        {
                            // TODO: Handle specific packets

                            packet = Packet.ReadPacket(m_Socket);
                        }
                    }
                    catch (Exception ex)
                    {
                        m_Logger.LogError(ex.Message);
                        Disconnect();
                        return;
                    }
                }

The code can be viewed with the tag Reading a Packet.

Running the server at this point gives us the same output, but if you attach a debugger and step through the code, you can see that it is reading the first packet of data! All we need to do next, is look at the packet type, and being processing! So, what is the packet? This is the first part of the Key Exchange Method. Which we will cover in the next section: Key Exchange