Špiónská aplikace v C# - Server, dokončení - 3. díl
= Článek pokračuje přesně tam, kde minulý skončil =
Metodě předáme SslStream, z kterého budeme číst. Metoda nám vrátí MemoryStream obsahující data, co klient poslal a jako out parametr vrátíme ještě hlavičku. Metoda je myslím celkem jasná. Vytvoříme BinaryWritery a Readery, pak přečteme long, abychom věděli kolik bajtů má zpráva a pak dva bajty jako hlavičku. Pokud klient poslal hlavičku, že se chce odpojit, vyhodíme výjimku a za chvíli si povíme, jak ji zpracujeme. Pokud je velikost zprávy 0, nemá cenu dále pokračovat, tak vrátíme prázdný MemoryStream. Jinak bajt po bajtu přečteme zprávu a data dáme do MemoryStreamu, abychom se vyhnuli OutOfMemoryExceptionům. Na závěr přičteme k počítadlu počet bajtů, které jsme přečetli, resetujeme Stream a vrátíme ho.
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(); header = new byte[2]; header[0] = networkReader.ReadByte(); header[1] = networkReader.ReadByte(); if (header[0] == OwnerID.SERVER_INTERNAL && header[1] == TypeID.BYE) throw new Exception("Client said: \"bye!\""); if (bufferSize == 0) return new MemoryStream(); while (bufferSize != 0) { memoryWriter.Write(networkReader.ReadByte()); bufferSize--; } uncompressedBPS += result.Length; result.Position = 0; return result; }
Metodu si přetížíme, abychom si usnadnili práci v další metodě.
private string ReadString(Stream securedStream) { byte[] header = new byte[2]; return GetString(Read(securedStream, out header).ToArray()); }
Nyní máme už vytvořený konstruktor, odesílací vlákno a základní věci, dále musíme udělat naslouchací vlákno. Naslouchací vlákno nebude fyzicky zpracovávat data od klientů, jen se s ním spojí, pošle mu certifikát s veřejným klíčem, a když ověření proběhne v pořádku, vytvoří pro něj nové vlákno. Nejdříve tedy začneme naslouchat a pak budeme prostě spát, dokud se něco nepřipojí. Až se někdo připojí, musíme ještě připojení zašifrovat. Pošleme tedy délku certifikátu a pak vlastní certifikát, co jsme si vygenerovali dříve. MUSÍ SE JEDNAT O CERTIFIKÁT OBSAHUJÍCÍ POUZE VEŘEJNÝ KLÍČ! Jinak by celé šifrování ztratilo smysl… Když klient má certifikát a můžeme tedy komunikovat šifrovaně, nezbývá nic jiného, než zprovoznit SslStream za pomoci NetworkStreamu z našeho TcpClienta. SslStream ovšem zavře náš původní NetworkStream, a proto jsme si na začátku vytvářeli strukturu Connection – potřebujeme vždy k TcpClientovi jeho SslStream. Pak se autentifikujeme svým privátním certifikátem a vytvoříme pro klienta nové vlákno, které již nyní bude obsluhovat jeho požadavky. Inicializace spojení je tímto hotova.
private void ListenThread() { try { this.tcpListener.Start(); } catch (Exception e) { Console.WriteLine(e.Message); } while (true) { try { TcpClient connection = tcpListener.AcceptTcpClient(); using (FileStream cert = File.OpenRead(@"cert-public.pem")) { BinaryWriter writer = new BinaryWriter(connection.GetStream()); 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; t.Start(new Connection(sslStream, connection)); } catch (Exception e) { Console.WriteLine(e.Message); } } }
Jako poslední musíme vytvořit vlastní klientské vlákno. Rozdělím jej na dvě části. V první části ověříme klienta ještě „naším“ heslem a dostaneme od něj jméno. Na začátku této metody máme tedy funkční spojení, ovšem server bude klientovi odpovídat, jen když klient ještě pošle heslo. Jméno se zase hodí, když budeme programovat s touto knihovnou např. chatovací aplikaci. Zase si definujeme pár konstant – pokud klient poslal správné nebo špatné heslo.
public static readonly byte AUTH_OK = 255; public static readonly byte AUTH_ERR = 254;
Dokud tedy připojení funguje, bude se provádět tato metoda stále dokola. Pokud v kolekci připojených klientů ten náš není, znamená to, že ještě heslo neposlal. Heslo a jméno jsou první dvě věci, co musí každý klient poslat, takže použijeme přetížení metody Read a přečteme heslo. Pokud heslo souhlasí, přečteme ještě jméno a přidáme si ho do kolekce. Na závěr ještě informujeme klienta o výsledku a spustíme event, že se klient přihlásil.
private void ClientThread(object data) { Connection connection = (Connection)data; string name = ""; while (connection.client.Connected) { if (!connections.Values.Contains(connection)) { try { if (password != "") { if (ReadString(connection.securedStream) == password) { name = ReadString(connection.securedStream); 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);
Druhá část je jednoduchá, spustí se, pokud klient už je v kolekci (a tím pádem se už přihlásil). Přečte od něj zprávu (až nějakou pošle) a pak ji prostě předá eventu. Pokud nám čtecí funkce vyhodila výjimku, cyklus ukončíme. To samé nastane, pokud nám klient pošle špatné heslo, či se někde jinde vyskytne výjimka, z čehož vyplývá, že dokud vše funguje jak má, cyklus běží. Když se cyklus ukončí, něco se pokazilo a klient se musí odpojit. Odstraníme ho tedy z kolekce a spustíme příslušný event.
} 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) { Console.WriteLine(e.Message); connections.Remove(name); break; } } } if (ClientLeft != null) ClientLeft(name, connection); }
No a to je vlastně „všechno“. Možná to na první pohled vypadá složitě, ale teď máme na použití krásně jednoduchou třídu, ke které zvenčí jen přiřadíme metody k eventům a voláme Send(). V příštím díle si naprogramujeme klienta.
Server si můžete stáhnout v archivu pod článkem.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkamiStaženo 1584x (16.81 kB)