using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Runtime; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Security.Cryptography.X509Certificates; using System.Security.Authentication; using System.IO.Compression; namespace SharpNet { public class Server { /// /// Delegate for recieving data /// /// Always two bytes /// /// /// public delegate void DataRecieved(byte[] header, MemoryStream message, string name, Connection sender); public delegate void ClientEvent(string name, Connection connection); public delegate void NewBPS(long uncompressed); public event DataRecieved MessageRecieved; public event ClientEvent ClientLoggedIn; public event ClientEvent ClientLeft; public event NewBPS NewBytesPerSecond; private long compressedBPS = 0; private long uncompressedBPS = 0; public string password = ""; public Dictionary connections = new Dictionary(); private TcpListener tcpListener; private List tasks = new List(); public IPAddress address { get { return ((IPEndPoint)tcpListener.LocalEndpoint).Address; } } public int port { get { return ((IPEndPoint)tcpListener.LocalEndpoint).Port; } } public Connection this[int i] { get { return connections.ElementAt(i).Value; } protected set { } } /// /// Initiates the server, runs threads etc. /// /// IP adress on which the server will be listening /// IP adress on which the server will be listening /// Password required from client, else the server will not even reply to the client. Password can be maximaly 100 characters long. /// Key, which will be used to encrypt the communication public Server(IPAddress address, int port, string password) { try { this.password = password; tcpListener = new TcpListener(address, port); Thread listenThread = new Thread(ListenThread); // we don't want to block main thread listenThread.Name = "listenThread"; listenThread.IsBackground = true; // it will not block exiting the application listenThread.Start(); Thread sendThread = new Thread(SendThread); // we don't want to block main thread sendThread.Name = "sendThread"; sendThread.IsBackground = true; // it will not block exiting the application sendThread.Start(); // bytes per second counter System.Timers.Timer timer = new System.Timers.Timer(); timer.Elapsed += timer_Elapsed; timer.Interval = 1000; timer.Enabled = true; } catch (Exception e) { Console.WriteLine(e.Message); } } void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { if (NewBytesPerSecond != null) NewBytesPerSecond(uncompressedBPS); uncompressedBPS = 0; } private void ListenThread() { try { this.tcpListener.Start(); } catch (Exception e) { Console.WriteLine(e.Message); } Console.WriteLine("Listening..."); while (true) { try { TcpClient connection = tcpListener.AcceptTcpClient(); // thread will sleep until something connects using (FileStream cert = File.OpenRead(@"cert-public.pem")) // load public key certificate { BinaryWriter writer = new BinaryWriter(connection.GetStream()); // send the certificate containing the public key writer.Write(cert.Length); cert.Position = 0; cert.CopyTo(connection.GetStream()); } SslStream sslStream = new SslStream(connection.GetStream()); sslStream.AuthenticateAsServer(new X509Certificate2(@"cert.pfx", password), false, SslProtocols.Default, false); Thread t = new Thread(ClientThread); t.IsBackground = true; // it will not block exiting the application t.Start(new Connection(sslStream, connection)); } catch (Exception e) { Console.WriteLine(e.Message); } } } private void SendThread() { while (true) { if (tasks.Count != 0) { lock (tasks) { foreach(Action task in tasks) { task(); } tasks.Clear(); Collect(); } } } } private void ClientThread(object data) { Connection connection = (Connection)data; // I swear it's a Connection :-) string name = ""; //Console.WriteLine("Client connected from" + ((IPEndPoint)connection.Client.RemoteEndPoint).ToString()); while (connection.client.Connected) { if (!connections.Values.Contains(connection)) // if current connection is not in the collection, it hasn't logged in { try { if (password != "") // if the password is set { if (ReadString(connection.securedStream) == password) // first thing the client must send is the password { name = ReadString(connection.securedStream); // then it sends the name connections.Add(name, connection); Send(connection.securedStream, new MemoryStream(), new byte[2] { OwnerID.SERVER_INTERNAL, TypeID.AUTH_OK }); } else { Send(connection.securedStream, new MemoryStream(), new byte[2] { OwnerID.SERVER_INTERNAL, TypeID.AUTH_ERR }); break; } } else { name = ReadString(connection.securedStream); connections.Add(name, connection); Send(connection.securedStream, new MemoryStream(), new byte[2] { OwnerID.SERVER_INTERNAL, TypeID.AUTH_OK }); } } catch (Exception e) { Console.WriteLine(e.Message); } if (ClientLoggedIn != null) ClientLoggedIn(name, connection); // alright, stuff set up, ready to communicate //Console.WriteLine("Client from " + ((IPEndPoint)connection.Client.RemoteEndPoint).ToString() + " logged in as " + name); } else { try { byte[] header = new byte[2]; MemoryStream stream = Read(connection.securedStream, out header); if (MessageRecieved != null) MessageRecieved(header, stream, name, connection); } catch (Exception e) // when client disconnects, exception is thrown and catch breaks main loop { Console.WriteLine(e.Message); connections.Remove(name); break; } } } if (ClientLeft != null) ClientLeft(name, connection); //Console.WriteLine("Client from " + ((IPEndPoint)connection.Client.RemoteEndPoint).ToString() + " disconnected"); } /// /// Overload for Send() accepting a string as 2nd parameter instead of a MemoryStream. /// Decodes the string to a byte array using UTF-8 encoding and sends it to a client. /// /// TcpClient whom to send the message /// UTF-8 encoded string to send /// Header public void Send(Stream securedStream, string data, byte[] header) { Send(securedStream, new MemoryStream(GetBytes(data)), header); } /// /// Sends all the bytes from provided Stream to secified TcpClient /// /// TcpClient whom to send these bytes /// Stream to send the data from /// Header public void Send(Stream securedStream, Stream data, byte[] header) { lock (tasks) { tasks.Add(delegate { try { if (data.Length > 0) { BinaryWriter writer = new BinaryWriter(securedStream); writer.Write(data.Length); writer.Write(header); data.Position = 0; data.CopyTo(securedStream); } else { BinaryWriter writer = new BinaryWriter(securedStream); writer.Write((long)0); writer.Write(header); } } catch (Exception e) { Console.WriteLine(e.Message); } }); } } /// /// Reads message from client byte by byte and puts it to a MemoryStream (to avoid memory exceptions) /// /// MemoryStream containing read bytes private MemoryStream Read(Stream securedStream, out byte[] header) { MemoryStream result = new MemoryStream(); BinaryReader networkReader = new BinaryReader(securedStream); BinaryWriter memoryWriter = new BinaryWriter(result); long bufferSize = networkReader.ReadInt64(); // first read the buffer size (64-bit int) header = new byte[2]; header[0] = networkReader.ReadByte(); // first byte - package owner header[1] = networkReader.ReadByte(); // second byte - type of package if (header[0] == OwnerID.SERVER_INTERNAL && header[1] == TypeID.BYE) throw new Exception("Client said: \"bye!\""); if (bufferSize == 0) return new MemoryStream(); Collect(); while (bufferSize != 0) // loop and read byte by byte { memoryWriter.Write(networkReader.ReadByte()); bufferSize--; } uncompressedBPS += result.Length; result.Position = 0; // reset return result; // and return the stream } /// /// Calls Read() method and decodes output bytes as UTF-8 string. VERY UNSAFE because of working with potentially big arrays! /// /// Bytes decoded as UTF-8 string private string ReadString(Stream securedStream) { //Collect(); byte[] header = new byte[2]; return GetString(Read(securedStream, out header).ToArray()); } public static string GetString(byte[] message) { return Encoding.UTF8.GetString(message); } public static byte[] GetBytes(string message) { return Encoding.UTF8.GetBytes(message); } /// /// Intiates Garbage Collection to avoid OutOfMemoryException. Should be called in each method working with big data. /// private void Collect() { GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); } private byte[] AES_Encrypt(byte[] bytesToBeEncrypted, byte[] passwordBytes) { byte[] encryptedBytes = null; // Set your salt here, change it to meet your flavor: // The salt bytes must be at least 8 bytes. byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; using (MemoryStream ms = new MemoryStream()) { using (RijndaelManaged AES = new RijndaelManaged()) { AES.KeySize = 256; AES.BlockSize = 128; var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000); AES.Key = key.GetBytes(AES.KeySize / 8); AES.IV = key.GetBytes(AES.BlockSize / 8); AES.Mode = CipherMode.CBC; using (var cs = new CryptoStream(ms, AES.CreateEncryptor(), CryptoStreamMode.Write)) { cs.Write(bytesToBeEncrypted, 0, bytesToBeEncrypted.Length); cs.Close(); } encryptedBytes = ms.ToArray(); } } return encryptedBytes; } private byte[] AES_Decrypt(byte[] bytesToBeDecrypted, byte[] passwordBytes) { byte[] decryptedBytes = null; // Set your salt here, change it to meet your flavor: // The salt bytes must be at least 8 bytes. byte[] saltBytes = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; using (MemoryStream ms = new MemoryStream()) { using (RijndaelManaged AES = new RijndaelManaged()) { AES.KeySize = 256; AES.BlockSize = 128; var key = new Rfc2898DeriveBytes(passwordBytes, saltBytes, 1000); AES.Key = key.GetBytes(AES.KeySize / 8); AES.IV = key.GetBytes(AES.BlockSize / 8); AES.Mode = CipherMode.CBC; using (var cs = new CryptoStream(ms, AES.CreateDecryptor(), CryptoStreamMode.Write)) { cs.Write(bytesToBeDecrypted, 0, bytesToBeDecrypted.Length); cs.Close(); } decryptedBytes = ms.ToArray(); } } return decryptedBytes; } private string MD5Hash(Stream input) { // step 1, calculate MD5 hash from input MD5 md5 = System.Security.Cryptography.MD5.Create(); byte[] hash = md5.ComputeHash(input); // step 2, convert byte array to hex string StringBuilder sb = new StringBuilder(); for (int i = 0; i < hash.Length; i++) { sb.Append(hash[i].ToString("X2")); } return sb.ToString(); } } }