Dydaktyka:
FeedbackTo jest stara wersja strony!
Do wygenerowania klucza prywatnego CA i certyfikatu CA oraz klucza prywatnego serwera i podpisanego przez wcześniej wygenerowane CA certyfikatu serwera użyj tego skryptu.
Zadanie 1
Wygeneruj potrzebne pliki za pomocą powyższego skryptu.
Wyświetl informacje o certyfikatach komendą:
openssl x509 -noout -text -certopt no_sigdump,no_pubkey -in <scieżka do certyfikatu>
Podobną rolę jak program nc
/ netcat
dla połączeń szyfrowanych spełniają
narzędzia openssl s_client
i openssl s_server
.
Również program socat
pozwala na tworzenie połączeń szyfrowanych.
openssl s_client [-verifyCAfile ca.crt] [-crlf] [-quiet|-brief] [-connect] put.poznan.pl:443
np: openssl s_client -brief mail.put.poznan.pl:imaps
-verifyCAfile <CA cert>
dodaje plik CA jako zaufany do sprawdzania certyfikatu serwera
-crlf
zamienia w danych odczytanych ze standardowego wejścia \n
na \r\n
-quiet
wyłącza zupełnie komunikaty diagnostyczne,
-brief
skraca komunikaty diagnostyczne; bez tych przełączników są pokazywane
długie komunkty
[-connect] <host>:<port>
wskazuje gdzie się połączyć; słowo kluczowe
-connect
można pominąć jeśli <host>:<port>
jest na końcu komendy
socat [open]ssl:put.poznan.pl:443,verify=0[,crlf] readline
np. socat ssl:mail.put.poznan.pl:imaps,verify=0 readline
verify=0
wyłącza sprawdzanie certyfikatu serwera
crlf
zamienia w wysyłanych danych \n
na \r\n
Zadanie 2
Połącz się z serwerem stron internetowych i otrzymaj od niego odpowiedź.
Kto podpisał certyfikat tego serwera? Czy certyfikat został zweryfikowany poprawnie?
Pamiętaj że do stworzenia prostego serwera połączeń szyfrowanych potrzebujesz1) klucza prywatnego i certyfikatu.
openssl s_server [-cert_chain chain.pem] -cert s.crt -key s.key [-quiet|-brief] [-naccept 1] -port 4000
-key <key>
podaje ścieżkę do klucza prywatnego
-cert <cert>
podaje ścieżkę do certyfikatu serwera
-cert_chain <chain>
podaje ścieżkę do łańcucha certyfikatów które podpisały certyfikat serwera
-naccept <num>
podaje po ilu połączeniach (obsługiwanych sekwencyjnie) serwer ma się zakończyć
socat openssl-listen:4000,key=server.key,cert=chain.pem,reuseaddr,verify=0[,fork] stdio
key=<key>
podaje ścieżkę do klucza prywatnego
cert=<chain>
podaje ścieżkę do (łańcucha) certyfikatów które podpisały certyfikat serwera
fork
nakazuje socatowi się sklonować po nawiązaniu połączenia; jeden proces
będzie obsługiwał nawiązane połączenie, drugi czekał na nowe;
uwaga: przy łączeniu drugiego końca z stdio
wszystkie procesy obsługujące
połączenia będą czytać z tego samego deskryptora (dane trafią niedeterministycznie
do jednego z nich)
Zadanie 3
Uruchom prosty serwer TLS używając wygenerowanych przez siebie certyfikatów.
Połącz się do niego klientem z konsoli.
Zadanie 4
Połącz się do prostego serwera TLS z przeglądarki internetowej.
Dlaczego przeglądarka uważa połączenie za niebezpieczne?
Możesz albo uruchomić serwer (z uprawnieniami administratora) na porcie
443 i w przeglądarce podać https://<nazwa domenowa>/
, lub uruchomić na
wybranym porcie i podać https://<nazwa domenowa>:<numer portu>/
Zadanie 5
Zainstaluj certyfikat swojego CA w przeglądarce. Następnie połącz się podając
jako nazwę domenową localhost
oraz lab-net-▒▒
.
Czy przeglądarka teraz ufa serwerowi?
Biblioteki OpenSSL są oparte o abstrakcję wejścia/wyjścia
BIO.
Tworząc kod używający biblioteki OpenSSL do szyfrowanych połączeń sieciowych TLS
(libssl)
programista musi najpierw stworzyć zwykłe połączenie TCP. Może do tego użyć
albo abstrakcji BIO (klient,
serwer),
albo zwykłego interfejsu gniazd.
Uwaga: tutaj jest opisana ta druga opcja. Często kod przykładowych programów
używa do obsługi gniazd TCP abstrakcji OpenSSL – zwracaj na to uwagę.
Następnie programista musi na już nawiązanym połączeniu TCP ustanowić połączenie TLS.
W tym celu najpierw należy stworzyć "obiekt" SSL, następnie powiązać go z
istniejącym połączeniem TCP (dla zwykłego interfejsu gniazd metodą
SSL_set_fd
).
Dalszy sposób użycia "obiektu" SSL
do nawiązania bądź przyjęcia połączenia
TLS jest opisany w materiałach z wykładów.
OpenSSL nie wspiera równoległego wykonywania operacji na tym samym "obiekcie"
z wielu wątków (por.
man openssl-threads).
W szczególności nie wolno wykonywać równolegle SSL_read i SSL_write na tym samym
"obiekcie" SSL (i deweloperzy nie są chętni tego zmieniać, bo wymagałoby
to dużych zmian w bibliotece – patrz
ten issue).
Nie należy też zmieniać ustawień "obiektu" SSL_CTX po jego pierwszym użyciu do stworzenia "obiektu" SSL.
Do połączenia skompilowanego kodu używającego OpenSSL do połączeń TLS
trzeba linkować do bibliotek libssl
oraz libcrypto
, np:
c++ prog.cpp -o prog -lssl -lcrypto
Przy użyciu CMake należy dodać do pliku CMakeLists.txt
wyszukanie biblioteki
OpenSSL oraz linkowanie wybranego celu do niej:
find_package ( OpenSSL 1.1 REQUIRED ) target_link_libraries ( prog OpenSSL::SSL )
(Bez tego na etapie linkowania zaczną pojawiać się błędy
undefined reference to…
z nazwami funkcji bibliotecznych OpenSSL.)
#include <stdio.h> #include <netinet/in.h> #include <sys/socket.h> #include <unistd.h> #include <netdb.h> #include <openssl/ssl.h> int main(int argc, char **argv) { SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); SSL *ssl = SSL_new(ctx); struct hostent *he = gethostbyname(argv[1]); struct sockaddr_in sa; sa.sin_family = AF_INET; sa.sin_port = htons(atoi(argv[2])); memcpy(&sa.sin_addr.s_addr, he->h_addr, he->h_length); int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); connect(fd, (struct sockaddr *)&sa, sizeof(sa)); SSL_set_fd(ssl, fd); SSL_connect(ssl); SSL_write(ssl, "Hello, Server!\n", 15); char buf[1024]; int count = SSL_read(ssl, buf, sizeof(buf)); write(1, buf, count); SSL_shutdown(ssl); close(fd); SSL_free(ssl); SSL_CTX_free(ctx); return 0; }
Zadanie 6
Skompiluj powyższy kod. Uruchom klienta łącząc się do serwera uruchomionego
przy użyciu openssl s_server …
lub socat openssl-listen:…
.
#include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #include <unistd.h> #include <openssl/ssl.h> int main(int argc, char **argv) { SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); SSL_CTX_use_certificate_chain_file(ctx, "chain.pem") == 1 || fprintf(stderr, "problem with chain.pem\n"); //SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) == 1 || fprintf(stderr, "problem with server.crt\n"); SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) == 1 || fprintf(stderr, "problem with server.key\n"); struct sockaddr_in sa = {0}; sa.sin_family = AF_INET; sa.sin_addr.s_addr = INADDR_ANY; sa.sin_port = htons(atoi(argv[1])); int sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); bind(sfd, (struct sockaddr *)&sa, sizeof(sa)) == 0 || fprintf(stderr, "problem with bind\n"); listen(sfd, 10); while (1) { int cfd = accept(sfd, NULL, NULL); SSL *ssl = SSL_new(ctx); SSL_set_fd(ssl, cfd); SSL_accept(ssl); char buf[1024]; SSL_read(ssl, buf, sizeof(buf)); SSL_write(ssl, "Welcome to the SSL/TLS server!\n", 31); SSL_shutdown(ssl); SSL_free(ssl); close(cfd); } }
Zadanie 7
Skompiluj powyższy kod i uruchom serwer. Upewnij się że wszystkie klucze
i certyfikaty zostały poprawnie załadowane i serwer rozpoczął nasłuchiwanie
na właściwym porcie.
Następnie połącz się klientem uruchomionym z terminala oraz przeglądarką
internetową.
#include <cstdio> #include <netinet/in.h> #include <string> #include <sys/socket.h> #include <unistd.h> #include <openssl/ssl.h> void die(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); exit(1); } std::string craftHttpAnswer(); int main(int argc, char **argv) { if (argc != 2) die("Usage: %s <port>\n", argv[0]); // Wybiera konfigurację dla serwera SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY); // Ładuje klucz prywatny serwera if (SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) != 1) die("Loading Private Key from \"server.key\" failed\n"); // Ładuje certyfikaty if (SSL_CTX_use_certificate_chain_file(ctx, "chain.pem") != 1) die("Loading certs from \"chain.pem\" failed\n"); // Tworzy gniazdo i rozpoczyna nasłuchiwanie - zwykły kod z API gniazd int serv = socket(AF_INET, SOCK_STREAM, 0); const int one = 1; setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); sockaddr_in sa = {}; sa.sin_family = AF_INET; sa.sin_port = htons(atoi(argv[1])); if (bind(serv, (sockaddr *)&sa, sizeof(sa)) != 0) die("bind failed: %s\n", strerror(errno)); listen(serv, 1); while (1) { // przyjmuje nowe połączenie int cli = accept(serv, 0, 0); // tworzy warstwę szyfrującą i wykonuje handshake SSL *encryption_layer = SSL_new(ctx); SSL_set_fd(encryption_layer, cli); if (SSL_accept(encryption_layer) != 1) { fprintf(stderr, "TLS handshake failed!\n"); continue; } std::string answer = craftHttpAnswer(); // przesyła odpowiedź if (SSL_write(encryption_layer, answer.c_str(), answer.length()) <= 0) { fprintf(stderr, "Problems with write!\n"); continue; } // zamyka warstwę szyfrującą SSL_shutdown(encryption_layer); // zamyka gniazdo shutdown(cli, SHUT_RDWR); close(cli); SSL_free(encryption_layer); } } #include <chrono> #include <format> std::string craftHttpAnswer(void) { std::string content = std::format( "{:%Y.%m.%d %H:%M:%S}\r\n", std::chrono::system_clock::now()); return std::format( "HTTP/2 200\r\n" "content-type: text/plain\r\n" "content-length: {}\r\n" "\r\n" "{}", content.length(), content); }
Zadanie 8 Powtórz poprzednie zadanie używając kodu tego serwera.
Poniższy kod klienta:
SSL_…
#include <cstdio> #include <fcntl.h> #include <netdb.h> #include <poll.h> #include <string> #include <sys/socket.h> #include <unistd.h> #include <openssl/err.h> #include <openssl/ssl.h> void die(const char *format, ...) { va_list args; va_start(args, format); vfprintf(stderr, format, args); va_end(args); exit(1); } int main(int argc, char **argv) { if (argc != 3) die("Usage: %s <ip> <port>\n", argv[0]); // tworzy gniazdo i nawiązuje połączenie TCP addrinfo *res; if (getaddrinfo(argv[1], argv[2], 0, &res)) die("getaddrinfo failed\n"); int cli = socket(res->ai_family, SOCK_STREAM, 0); if (connect(cli, res->ai_addr, res->ai_addrlen) != 0) die("connect failed\n"); freeaddrinfo(res); /* ───────────────────────────────────────────────────────────────────── */ // wybiera konfigurację warstwy szyfrującej SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); // podstawowe ustawienia dla klienta TLS SSL_CTX_set_default_verify_paths(ctx); // nakazuje użyć systemowych certyfikatów CA // żeby przetestować weryfikację certyfikatu bez instalowania certyfikatu CA w systemie: // ustaw zmienną środowiskową MY_ROGUE_CA ma ścieżkę do własnego certyfikatu CA if (getenv("MY_ROGUE_CA")) SSL_CTX_load_verify_file(ctx, getenv("MY_ROGUE_CA")); /* ───────────────────────────────────────────────────────────────────── */ // tworzy obiekt reprezentujący warstwę szyfrującą i nakazuje mu czytać/pisać do pliku gniazda SSL *encryption_layer = SSL_new(ctx); SSL_set_fd(encryption_layer, cli); // informuje bibliotekę jakiej nazwie powinien odpowiadać certyfikat SSL_set1_host(encryption_layer, argv[1]); // ustanawia komunikację szyfrowaną TLS (na wskazanym połączeniu TCP) wykonując handshake if (SSL_connect(encryption_layer) != 1) die("TLS handshake failed!\n"); // odbiera wyniki weryfikacji poprawności otrzymanego od serwera certyfikatu long serverCertStatus = SSL_get_verify_result(encryption_layer); if (serverCertStatus != X509_V_OK) fprintf(stderr, "[Warning: server certificate verification failed - %s!]\n", X509_verify_cert_error_string(serverCertStatus)); /* ───────────────────────────────────────────────────────────────────── */ // fragment switcha sprawdzającego kody błędów, wspólna cześć dla read i write #define MY_ERROR_HANDLING_SWITCH_MISC \ case SSL_ERROR_ZERO_RETURN: \ fprintf(stderr, "[Server terminated encryption layer]\n"); \ goto close_connection; \ default: \ fprintf(stderr, "[Some fatal error]\n"); \ ERR_print_errors_fp(stderr); \ goto close_connection; pollfd pfds[2]{ {.fd = 0, .events = POLLIN}, {.fd = cli, .events = POLLIN}, }; // zdarzenia które są potrzebne do odczytania i zapisu danych przez warstwę szyfrującą int readNeeds = POLLIN; // uwaga: TLS może potrzebować zrobić zapis do gniazda żeby odebrać dane int writeNeeds = 0; // uwaga: TLS może potrzebować zrobić odczyt do gniazda żeby wysłać dane // bufor na odbierane dane char recvBuffer[255]; // bufory na wysłane dane std::string dataBeingWritten; std::string dataWaitingToBeWritten; // uwaga: jeśli nieblokujące SSL_write się nie powiodło bo musiałoby czekać, // trzeba je powtórzyć z identycznymi argumentami; stąd dwa bufory // ustawia gniazdo w tryb nieblokujący (!) fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) | O_NONBLOCK); // funkcje SSL_read i SSL_write mogą wielokrotnie wywoływać send i recv na // gnieździe, co przy blokującym trybie i użyciu zdarzeń mogłoby zablokować // program na obsłudze któregoś zdarzenia; stąd tryb nieblokujący while (1) { poll(pfds, 2, -1); if (pfds[0].revents) { char buf[256]; int c = read(0, buf, 255); if (c > 0) { buf[c] = 0; dataWaitingToBeWritten += buf; writeNeeds |= POLLOUT; // dane będą wysłane dopiero w następnej iteracji pętli zdarzeń (to upraszcza kod) } else { // koniec pliku lub błąd na standardowym wejściu if (dataBeingWritten.empty() && dataWaitingToBeWritten.empty()) { fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) & ~O_NONBLOCK); SSL_shutdown(encryption_layer); // SSL_shutdown zamyka tylko wysyłanie danych; dane można // odbierać aż druga strona nie zrobi SSL_shutdown fcntl(cli, F_SETFL, fcntl(cli, F_GETFL) | O_NONBLOCK); pfds[0].fd = -1; } else usleep(1000); // aktywne czekanie (powtarzana próba odczytu z zamkniętego standardowego // wejścia) na wepchnięcie do bufora nadawczego TCP zaszyfrowanych danych // przed włożeniem tam wiadomości zamknięciem warstwy szyfrującej jest tutaj // pozostawione dla uproszczenia kodu } } /* ───────────────────────────────────────────────────────────────────── */ bool isThereAnyDataToWrite = (!dataBeingWritten.empty() || !dataWaitingToBeWritten.empty()); if (pfds[1].revents & writeNeeds && isThereAnyDataToWrite) { writeNeeds = 0; if (dataBeingWritten.empty()) std::swap(dataBeingWritten, dataWaitingToBeWritten); int result = SSL_write(encryption_layer, dataBeingWritten.c_str(), dataBeingWritten.length()); // obsługa niepowodzenia funkcji SSL_write switch (SSL_get_error(encryption_layer, result)) { case SSL_ERROR_NONE: break; case SSL_ERROR_WANT_READ: writeNeeds = POLLIN; break; case SSL_ERROR_WANT_WRITE: writeNeeds = POLLOUT; break; MY_ERROR_HANDLING_SWITCH_MISC } ERR_clear_error(); // obsługa jeśli udało się wysłać dane if (result > 0) { // domyślnie SSL_write kończy się sukcesem dopiero jeśli wysłano // wszystkie dane, nawet jeśli gniazdo jest w trybie nieblokującym dataBeingWritten.clear(); if (!dataWaitingToBeWritten.empty()) writeNeeds = POLLOUT; } } /* ───────────────────────────────────────────────────────────────────── */ if (pfds[1].revents & readNeeds) { readNeeds = POLLIN; int result = SSL_read(encryption_layer, recvBuffer, 255); if (result > 0) { // odczyt się powiódł == SSL odczytał blok danych; rozmiar bloku // może być mniejszy bądź większy niż bufor w SSL_read. write(1, recvBuffer, result); // Jeżeli blok był większy niż bufor, to reszta danych czeka // w buforach warstwy szyfrującej - funkcja SSL_read może zwrócić // kolejną porcję danych bez wywoływania recv na gnieździe while (int remaining = SSL_pending(encryption_layer)) { result = SSL_read(encryption_layer, recvBuffer, remaining < 255 ? remaining : 255); write(1, recvBuffer, result); } } else { // obsługa niepowodzenia funkcji SSL_read switch (SSL_get_error(encryption_layer, result)) { case SSL_ERROR_NONE: case SSL_ERROR_WANT_READ: break; case SSL_ERROR_WANT_WRITE: readNeeds = POLLOUT; break; MY_ERROR_HANDLING_SWITCH_MISC } ERR_clear_error(); } } /* ───────────────────────────────────────────────────────────────────── */ pfds[1].events = readNeeds | writeNeeds; } close_connection: shutdown(cli, SHUT_RDWR); close(cli); return 0; }
Zadanie 9 Przeczytaj kod klienta. Zwróć szczególnie uwagę na komentarze – przybliżają działanie API biblioteki OpenSSL. Przetestuj kod.
#include <cstdio> #include <openssl/ssl.h> // Uwaga - kod dla uproszczenia jest pozbawiony obsługi błędów! int main(int argc, char **argv) { // przygotowuje obiekt SSL ustawia do użycia w połączeniu klienta TLS SSL_CTX *ssl_ctx = SSL_CTX_new(TLS_client_method()); SSL_CTX_set_default_verify_paths(ssl_ctx); SSL *ssl = SSL_new (ssl_ctx); SSL_set_connect_state(ssl); // tworzy obiekt 'Basic Input/Output' typu połączenie sieciowe BIO* tcp_connection = BIO_new_connect(argv[1]); // łączy się do podanego adresu (jako "host:port") BIO_do_connect(tcp_connection); // tworzy obiekt 'Basic Input/Output' typu filtr TLS na istniejącym BIO BIO *tls_connection = BIO_new(BIO_f_ssl()); // wiąże ten obiekt ze wskazanym, wcześniej skonfigurowanym obiektem SSL BIO_set_ssl (tls_connection, ssl, BIO_NOCLOSE); // podkłada pod spód tego filtra obiekt połączenia TCP BIO_push(tls_connection, tcp_connection); // wykonuje połączenie TLS BIO_do_handshake(tls_connection); // sprawdza stan certyfikatu serwera if(long err = SSL_get_verify_result(ssl)) fprintf(stderr, "[cert error: %s]\n", X509_verify_cert_error_string(err)); // wymienia dane po połączeniu TLS BIO_puts(tls_connection, "Hello World!\n"); char buf[1024]; BIO_get_line(tls_connection, buf, 1024); printf("%s", buf); // zamyka połączenie TLS BIO_ssl_shutdown(tls_connection); BIO_free(tls_connection); // połączenie TCP można tutaj dalej używać do przesyłania (nieszyfrowanych) // danych - w BIO_set_ssl podano żeby nie zamykać gniazda TCP (BIO_NOCLOSE) // zamyka połączenie TCP BIO_free(tcp_connection); return 0; }
Zadanie 10 Poniżej znajduje się program realizujący prostą sieciową
bazę klucz-wartość
obsługujący sekwencyjnie klientów. Program przyjmuje od klientów dwa rodzaje
żądań: ustawienie klucza (format żądania: klucz wartość
) i pobranie
poprzednio ustawionej wartości (format żądania: klucz
).
Przerób program tak, by używał połączeń TLS.
#include <cstdio> #include <netdb.h> #include <netinet/in.h> #include <string> #include <sys/socket.h> #include <unistd.h> #include <unordered_map> #include <openssl/ssl.h> std::unordered_map<std::string, std::string> key_value_database; int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "Usage: %s <port>\n", argv[0]); return 1; } int serv = socket(AF_INET, SOCK_STREAM, 0); const int one = 1; setsockopt(serv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); sockaddr_in sa = {}; sa.sin_family = AF_INET; sa.sin_port = htons(atoi(argv[1])); if (bind(serv, (sockaddr *)&sa, sizeof(sa))) { perror("bind failed"); return 1; } listen(serv, 1); while (1) { sockaddr_in ca; socklen_t l = sizeof(ca); int cli = accept(serv, (sockaddr *)&ca, &l); char clientName[NI_MAXHOST], clientPort[NI_MAXSERV]; getnameinfo((sockaddr *)&ca, l, clientName, NI_MAXHOST, clientPort, NI_MAXSERV, 0); printf("New client [%s:%s]\n", clientName, clientPort); while (1) { char request[1025]{}; int count = read(cli, request, 1024); if (count <= 0) break; if (request[count - 1] == '\n') request[count - 1] = '\0'; if (!strlen(request)) continue; char *firstSpace = strchr(request, ' '); if (firstSpace != nullptr) { *firstSpace = '\0'; char *key = request; char *value = firstSpace + 1; key_value_database[key] = value; printf("[%s:%s] assigned to '%s' value '%s'\n", clientName, clientPort, key, value); } else { char *key = request; std::string value; if (key_value_database.count(key)) value = key_value_database[key]; else value = "(null)"; std::string response = std::string("[") + key + "] " + value + "\n"; write(cli, response.data(), response.length()); printf("[%s:%s] read from '%s' value '%s'\n", clientName, clientPort, key, value.data()); } } printf("Client [%s:%s] gone\n", clientName, clientPort); shutdown(cli, SHUT_RDWR); close(cli); } }
Zadanie 11 Zmień program z poprzedniego zadania tak, żeby program obsługiwał
wielu klientów naraz używając zdarzeń. Załóż że po zgłoszeniu odpowiedniego
zdarzenia przez select/pool/epoll_wait wszystkie funkcje SSL_…
kończą się
bez czekania (sukcesem lub błędem uniemożliwiającym dalszą komunikację).
Zadanie 12 Zmień program z poprzedniego zadania tak, by działał bez
założenia o natychmiastowym zakończeniu funkcji SSL_…
po zgłoszeniu
odpowiedniego zdarzenia.
Zauważ że w tym zadaniu musisz używać gniazd w trybie nieblokującym
i analizować błędy na stosie błędów biblioteki OpensSSL (patrz przykładowy
kod klienta wyżej).