# PSI - Projekt, raport ostateczny
<div style="text-align:right"><b>Data</b>: 18.01.2021, <b>Wersja</b>: 2.0, <b>Wariant</b>: W21</div>
Zespół:
- Przemysław Rozwałka (lider) - przemyslaw.rozwalka.stud@pw.edu.pl
- Jakub Budrewicz
- Filip Drygaś
- Marcel Jarosz
Treść
> Napisać program obsługujący prosty protokół P2P (Peer-to-Peer).
> Założenia:
> • Zasób to plik identyfikowany pewną nazwą, za takie same zasoby uważa się zasoby o takich samych nazwach.
> • Początkowo dany zasób znajduje się w jednym weźle sieci, następnie może być propagowany do innych węzłów w
> ramach inicjowanego przez użytkownika ręcznie transferu (patrz dalej) – raz pobrany zasób zostaje zachowany
> jako kopia.
> • Tak więc, po pewnym czasie działania systemu ten sam zasób może znajdować się w kilku węzłach sieci (na kilku
> maszynach).
> • Program ma informować o posiadanych lokalnie (tj. w danym węźle) zasobach i umożliwiać ich pobranie.
> • Program powinien umożliwać współbieżne:
> ◦ wprowadzanie przez użytkownika nowych zasobów – z lokalnego systemu plików,
> ◦ pobieranie konkretnych nazwanych zasobów ze zdalnego węzła,
> ◦ pobieranie zasobów „w tle” (także kilku jednocześnie),
> ◦ rozgłaszanie informacji o posiadanych lokalnie zasobach.
> • W przypadku pobierania zdalnego zasobu system sam (nie użytkownik) decyduje skąd zostanie on pobrany.
> • Zasób pobrany do lokalnego węzła jest kopią oryginału, kopia jest traktowana tak samo jak oryginał (są
> nierozróżnialne) – t.j.: istnienie kopii jest rozgłaszane tak samo jak oryginału.
> • Należy zwrócić uwagę na różne obsługę różnych sytuacji wyjątkowych – np. przerwanie transmisji spowodowane
> błędem sieciowym.
> • Lokalizacja zasobów ma następować poprzez rozgłaszanie – wskazówka: użyć prot. UDP, ustawić opcje gniazda
> SO_BROADCAST, wykorzystać adresy IP rozgłaszające (same bity “1” w części hosta).
> • Można dodatkowo wprowadzić skrót lub podpis, aby zapewnić jednoznaczność identyfikacji zasobów.
> • Interfejs użytownika – wystarczy prosty interfejs tekstowy, powinien on jednak obsługiwać współbiezny transfer
> zasobów (tj. Nie powinien się blokować w oczekiwaniu na przesłanie danego zasobu)
Wariant W21:
> Zasymulowac programowo błędy transmisji w zakresie rozgłaszania – tj. gubienie datagramów rozgłaszanych.
# 1. Interpretacja zadania
Mamy przygotować program do wymiany plików między użytkownikami w ramach pojedynczej podsieci stosując podejście peer-to-peer.
Założenia techniczne:
- Nazwa pliku o maksymalnej długości określonej w pliku konfiguracyjnym, jest niepowtarzalnym identyfikatorem zasobu w lokalnym repozytorium
- Dane o dostępności pliku zawierają również jego długość (w bajtach) oraz skrót SHA256 jego zawartości
- Program tworzy oraz dynamicznie aktualizuje listę aktywnych węzłów w sieci
- Lokalizacja zasobu odbywa się poprzez rozgłoszenie datagramu zawierającego nazwę szukanego pliku, a wszystkie węzły w sieci zwracają wynik zapytania
- W celu pobrania pliku, węzeł źródłowy wybierany jest losowo spośród wszystkich węzłów zawierających dany plik
- Po zakończeniu transferu, skrót pliku jest weryfikowany; jeżeli zostanie stwierdzona utrata integralności pliku, cały transfer jest ponawiany (węzeł źródłowy wybierany jest na nowo)
- Każdy plik pobrany przez dany węzeł automatycznie staje się udostępniany dla sieci
- Wszelkie meta-dane o pliku (skrót, status) przechowywane są w odpowiadającym pliku YAML w ukrytym folderze '.meta'
# 2. Opis funkcjonalny
Założenia funkcjonalne naszej implementacji:
- Pliki rozgłaszane są przez klientów, którzy pobrali je w pełni i zweryfikowali ich poprawność z hashem lub dodali plik ręcznie do listy rozgłaszanych plików
- Program działa w trybie interaktywnym
- Wyłączenie aplikacji powoduje:
- Przerwanie pobierania (po ponownym uruchomieniu programu, pobieranie jest wznawiane)
- Zapisanie listy rozgłaszanych plików (wraz z lokalizacjami tych plików)
- Interfejs użytkownika zrealizowany jest w konsoli i pozwala na:
- Wyświetlenie dokumentacji użytkowej - `help`
```
> help
Documented commands (type help <topic>):
========================================
add download exit help info peers remove search status
```
- Dodanie pliku - `add <file_path>`
````
> add /home/starwader/TempleOS.ISO
Added file TempleOS.ISO with digest 5d0fc944e5d89c155c0fc17c148646715bc1db6fa5750c0b913772cfec19ba26
````
- Pobranie pliku - `download <filename>`
``````
> download TempleOS.ISO
Searching... please wait
Files found in the network:
+----+--------------+-------------+---------+
| ID | Name | Fingerprint | From |
+----+--------------+-------------+---------+
| 0 | TempleOS.ISO | 5d0fc944e5 | 3 peers |
+----+--------------+-------------+---------+
Starting download...
``````
- Wyświetlenie informacji o pliku w lokalnym repozytorium - `info <file_name>`
```
> info windows
+---------+-------------+--------+------------+------------------------------+
| Name | Fingerprint | Status | Size | Path |
+---------+-------------+--------+------------+------------------------------+
| windows | 6911e83944 | READY | 5824122880 | /home/starwader/isos/windows |
+---------+-------------+--------+------------+------------------------------+
```
- Wyświetlenie listy znanych partnerów - `peers`
```
> peers
+----+------------+----------------------------+
| ID | IP address | Last updated |
+----+------------+----------------------------+
| 0 | 10.1.1.10 | 2022-01-16 19:49:58.089793 |
+----+------------+----------------------------+
| 1 | 10.1.1.53 | 2022-01-16 19:49:55.022034 |
+----+------------+----------------------------+
```
- Usunięcie pliku z repozytorium (nie z systemu plików) - `remove <file_name>`
```
> remove windows
Deleting file 'windows' with status 'ready'
```
- Wyszukanie pliku w sieci - `search <file_name>`
```
> search TempleOS.ISO
Searching... please wait
Files found in the network:
+----+--------------+-------------+---------+
| ID | Name | Fingerprint | From |
+----+--------------+-------------+---------+
| 0 | TempleOS.ISO | 5d0fc944e5 | 1 peers |
+----+--------------+-------------+---------+
```
- Sprawdzenie statusu programu - `status`:
```
> status
+----+--------------+-------------+------------+-------------+----------+-----------+
| ID | File name | Fingerprint | Size | Status | Progress | Peer(s) |
+----+--------------+-------------+------------+-------------+----------+-----------+
| 0 | kali.iso | 4a9bf66238 | 3789008896 | READY | --- | --- |
| 1 | windows.iso | 6911e83944 | 5824122880 | UPLOADING | --- | 1 clients |
| 2 | poc | dad1c4654c | 225 | READY | --- | --- |
| 3 | TempleOS.iso | 5d0fc944e5 | 17350656 | DOWNLOADING | 78.41% | 10.1.1.10 |
+----+--------------+-------------+------------+-------------+----------+-----------+
```
- Wyłączenie programu - `exit`
# 3. Opis i analiza stosowanych protokołów
## 3.1 Opis stosowanych protokołów
### Transfer plików (TCP)
Nasz protokół aplikacyjny TCP do transferu plików ma następujące cechy:
- jest protokołem tekstowym
- jest sesyjny, lecz bezstanowy (wszystkie informacje zawarte są w treści pojedynczego zapytania)
- zapytanie klienta składa się z linii zapytania oraz dowolnej liczby nagłówków w postaci `klucz: wartość`
- odpowiedź serwera składa się z linii statusu, dowolnej liczby nagłówków w postaci `klucz: wartość` oraz opcjonalnie zawiera dodatkową zawartość
- w ramach pojedynczego połączenia (sesji) obsługiwane jest jedno zapytanie
#### Założenia techniczne
Konstrukcja protokołu wzorowana jest na uproszczonych elementach protokołu HTTP.
Jako ogranicznik linii przyjmujemy kombinację znaków `\r\n` (`CR LF`), lecz dopuszczamy tolerancję również samego znaku `\n` (`LF`).
Łańcuchy znaków kodowane są w UTF-8.
Dodatkowe parametry zapytania i odpowiedzi podawane są jako nagłówki w formacie klucz-wartość z separatorem `: `, przy czym wielkość liter klucza nie ma znaczenia. Jeżeli dany nagłówek wystąpi kilka razy, uwzględniana jest jego ostatnia wartość.
Po opcjonalnej liście nagłówków wymagana jest jedna pusta linia.
#### Składnia zapytania
```
METHOD PATH
header1: value
header2: value
```
Każde zapytanie zawiera ścieżkę zasobu `PATH` - w naszym przypadku będzie to zawsze nazwa pliku.
`METHOD` określa charakter zapytania:
* `GET` - zwróć dane zasobu `PATH` wraz z jego zawartością
* `HEAD` - zwróć dane zasobu `PATH` bez zawartości
Nagłówki klienta o specjalnych znaczeniach:
* `If-digest: <alg>=<resource_hash>`: informuje serwer o wymaganym skrócie odpytywanego zasobu; jeżeli wersja zasobu na serwerze różni się, serwer powinien zwrócić błąd
* `Range: bytes <from>-<to>`: oczekuje od serwera przesłania tylko wskazanego fragmentu danego zasobu
#### Składnia odpowiedzi:
```
STATUS_CODE STATUS_MSG
header1: value
header2: value
RESPONSE_BODY
```
Każda odpowiedź zawiera linię statusu złożoną z trzycyfrowego kodu oraz krótkiej czytelnej wiadomości opisującej powodzenie zapytania.
Pierwsza cyfra kodu statusu pozwala skategoryzować status według konwencji:
*(pomijamy statusy `1xx`)*
* `2xx`: OK, zapytanie obsłużone
* `3xx`: póki co OK, ale klient powinien wykonać najpierw akcję wskazaną przez serwer
* `4xx`: błąd po stronie zapytania (klienta)
* `5xx`: błąd serwera
Przykładowe statusy:
* `200 OK` *(całe zapytanie poprawnie obsłużone)*
* `206 Partial content` *(w przypadku wysłania fragmentu zasobu wskazanego przez klienta w nagłówku `Range`)*
* `400 Bad request` *(błąd składniowy w zapytaniu)*
* `404 Not found` *(zasób nie istnieje)*
* `412 Precondition failed` *(w przypadku niespełnionego warunku `If-digest`)*
* `416 Invalid range` *(w przypadku niepoprawnego lub niespełnialnego przedziału w nagłówku `Range`)*
* `500 Server error` *(błąd serwera)*
Nagłówki serwera o specjalnych znaczeniach:
* `Content-type: <mime_type>`: typ zawartości odpowiedzi (`RESPONSE_BODY`); obsługujemy `text/plain` dla tekstu oraz `application/octet-stream` dla danych binarnych
* `Content-length: <num_bytes>`: długość zawartości odpowiedzi (`RESPONSE_BODY`)
* `Content-range: bytes <from>-<to>/<total length>`: długość fragmentu odpowiedzi w przypadku `206 Partial content`
* `Digest: <alg>=<resource_hash>`: skrót odpytywanego zasobu wyznaczony wskazanym algorytmem; obsługujemy tylko `SHA256`
`RESPONSE_BODY` jest opcjonalną zawartością odpowiedzi (o długości `Content-length`), w szczególności może zawierać dane binarne.
### Wykrywanie węzłów (UDP)
Do wykrywania węzłów oraz do wyszukiwania plików u partnerów wykorzystujemy protokół binarny, w którym wyróżniamy wiadomości:
* Do wykrywania węzłów:
* `HELLO` - datagram rozgłaszany przez klienta chcącego zaktualizować lub zainicjalizować listę partnerów
* `HERE` - datagram rozgłaszany przez każdego członka sieci co 10 sekund lub jako odpowiedź na komunikat `HELLO`, zawiera port lokalnego serwera repozytorium plików
* Do wyszukiwania plików:
* `FIND` - datagram rozgłaszany służący do wyszukiwania plików w sieci
* `FOUND` - bezpośredni komunikat od partnera o znalezieniu danego pliku
* `NOTFOUND` - bezpośredni komunikat od partnera informujący o nieznalezieniu danego pliku
W celu rozróżnienia datagramów oraz rodzaju zawartej wiadomości pochodzących od węzłów naszego protokołu, wprowadzamy nagłówek zawierający dwa *magiczne* bajty (stałe dla protokołu), jego wersję oraz ID wiadomości.
### Zarys struktur, dla czytelności w języku C
```c
#define byte unsigned char
#define HEADER_LEN 4
#define PROTO_VERSION 1
#define MAGIC_NUMBER 0xD16D
#define MAX_FILENAME_LENGTH 32
struct header {
uint16_t magic_number;
byte proto_version;
byte message_id;
};
struct msg_hello {/* empty */}; // HELLO(0x01)
struct msg_here { // HERE(0x02)
uint16_t unicast_port;
uint16_t tcp_port;
};
struct msg_file_data {// struct used in FIND(0x11), FOUND(0x12), NOTFOUND(0x13)
char file_name[MAX_FILENAME_LENGTH];
char file_hash[64];
uint64_t file_size;
};
```
## 3.2 Analiza stosowanych protokołów
### Protokół TCP (transfer pliku)
Wykorzystanie TCP gwarantuje nam spójność danych otrzymanych od węzła źródłowego. Weryfikacja skrótu pliku po pobraniu pozwala nam stwierdzić, że serwer wysłał odpowiedni plik. Zastosowanie skrótu pomaga też, gdy połączenie TCP ulegnie przerwaniu - jesteśmy w stanie podjąć próbę wznowienia pobierania pliku z gwarancją, że pod identyfikatorem zasobu nie istnieje teraz zmieniony plik.
### Protokół UDP (wykrywanie węzłów/plików)
#### Działanie w kontekście gubienia datagramów rozgłaszanych
- Węzły rozgłaszają co 10 sekund datagram `HERE`, a odrzucenie z listy partnerów następuje po 30 sekundach nieodebrania datagramu po drugiej stronie.
Gdy pierwsze rozgłoszenie węzła nie zostanie odebrane, to pozostanie wystarczająco czasu na dojście kolejnego rozgłoszenia. W ten sposób szansa, że usuniemy żywego partnera znacznie maleje.
- Na podobnej zasadzie działa oczekiwanie na odpowiedź partnerów na zapytanie `FIND` - w tym przypadku czekamy 2 sekundy na odpowiedź wszystkich partnerów z listy (`FOUND` lub `NOTFOUND`). W przypadku braku odpowiedzi, zapytanie zostaje powtórzone zwracając uwagę tylko na odpowiedzi od tych partnerów, od których wcześniej nie otrzymaliśmy żadnej odpowiedzi. Po 2 takich próbach usuwamy partnera z listy.
#### Wyszukiwanie/aktualizowanie listy partnerów
1. Każdy węzeł rozgłasza co 10 sekund lub po otrzymaniu komunikatu HELLO komunikat UDP: `HERE`
2. Każdy wezeł nasłuchuje na komunikaty `HERE` i zbiera listę partnerów w sieci
Po dodaniu nowego węzła do sieci, rozgłasza on komunikat `HELLO` i oczekuje na odpowiedzi `HERE` od hostów przez kilka sekund (inicjalizacja).
# 4. Schemat komunikacji węzłów
<img src="C:\Users\przem\Desktop\data\diagram.svg" alt="diagram" style="zoom: 67%;" />
# 5. Wykorzystane narzędzia
Do implementacji programu SimpleP2P użyliśmy Pythona w wersji 3.9. Program działa docelowo tylko na systemie Linux.
Wykorzystane biblioteki z biblioteki standardowej Pythona:
- `logging` - logowanie zdarzeń w modułach
- `struct` - serializacja struktur binarnych
- `hashlib` - obliczanie skrótów plików *(SHA256)*
- `socket` - obsługa gniazd
- `random` - liczby pseudolosowe
- `threading` - wielowątkowość
- `cmd`, `argparse` - interaktywny interfejs użytkownika
- `asyncio` - asynchroniczna obsługa strumieni (TCP oraz plików) w celu lepszego wykorzystania zasobów
Zewnętrzne zależności:
- `aiofile` - w pełni asynchroniczna obsługa plików
- `netifaces` - pozyskiwanie danych o interfejsach sieciowych
- `prettytable` - formatowanie tabel w interfejsie użytkownika
- `PyYAML` - obsługa plików yaml
Do ręcznego testowania protokołu TCP wykorzystano narzędzie `telnet`.
# 6. Implementacja
Program został podzielony na pięć logicznych moduł:
* `common`: zawiera funkcjonalność wspólną dla wszystkich pozostałych modułów, tj.: funkcje pomocnicze, modele oraz konfigurację
* `repository`: zajmuje się zarządzaniem stanami plików oraz ich przechowywaniem
* `file_transfer`: implementuje protokół TCP wraz z podstawową obsługą transferu plików
* `udp`: odpowiada za wszelką komunikację za pomocą datagramów; implementuje wyszukiwanie partnerów oraz plików
* `core`: główny moduł zawierający implementację głównego kontrolera oraz interfejs użytkownika
## 6.1 UDP
### Struktury
Plik `structs.py` zawiera klasy mapujące struktury binarne z biblioteki `struct` i metody ułatwiające ich obsługę.
### Datagramy
Plik `datagrams.py` zawiera klasy przechowujące wysyłane datagramy. Każdy datagram uzywany w komunikacji UDP przechowuje w sobie nagłówek `struct header` oraz strukturę wiadomości w zależności od typu datagramu - `struct message`.
Każdy datagram posiada metodę `to_bytes`, która zwraca ciąg bajtów nadający się do wysłania za pomocą gniazd.
### Gniazda
Plik `udp_socket.py` zawiera klasy `UdpSocket` oraz `BroadcastSocker(UdpSocket)`, które implementują kontrolę serwera UDP, obsługę socketów (biblioteka `socket` oraz `asyncio`), oraz pozwalają na równoległe wysyłanie datagramów.
Klasa `AsyncioDatagramProtocol(asyncio.DatagramProtocol)` implementuje właściwą funkcjonalność asynchronicznego odbierania datagramów na dane gniazdo oraz logikę odrzucania datagramów rozgłaszanych (wariant W21).
Klasa `UdpSocket` posiada tablicę `_receive_callbacks`, w której znajdują się wszystkie metody wywoływane w celu obsługi odebranych datagramów.
### Kontroler
Moduł UDP zawiera klasę `UdpController` który implementuje całą logikę komunikacji broadcast oraz unicast UDP.
### Filtrowanie datagramów rozgłaszanych pochodzących od siebie
Aby nasz program działał prawidłowo, należy odfiltrować wszystkie datagramy rozgłaszane wychodzące z naszego IP. Listę naszych IP pozyskujemy za pomocą biblioteki `netifaces`.
Filtrowanie datagramów rozgłaszanych jest zaimplementowane w klasie `AsyncioDatagramProtocol` - można nim sterować za pomocą stałej `BROADCAST_OMIT_SELF`.
### Gubienie datagramów rozgłaszanych (Wariant W21)
Gubienie datagramów rozgłaszanych zaimplementowano w `AsyncioDatagramProtocol`. Wartości zmiennych konfiguracyjnych można przekazać jako opcjonalne argumenty skryptu.
Rozpoczęcie gubienia datagramów rozgłaszanych następuje z prawdopodobieństwem `BROADCAST_DROP_CHANCE` procent.
Po rozpoczęciu trybu gubienia datagramów rozgłaszanych, program odrzuca kolejne przychodzące do niego datagramy `BROADCAST_DROP_IN_ROW` razy.
Przykłady działania zamieściliśmy w paragrafie **8.1** *Wariant W21 - Gubienie datagramów rozgłaszanych*.
## 6.2 Transfer plików - TCP
### Koncepcja
Zakładając, że protokół będzie służył w pierwszej kolejności do przesyłania plików z gotowymi metadanymi z repozytorium plików, istnieje duży zysk z wykorzystania podejścia nieblokującego w celu obsługi zapytań - zarówno dla odczytu pliku, jak i samego wysyłania przez sieć. W tym celu wykorzystana została biblioteka `asyncio` oferująca wsparcie do programowania asynchronicznego w przystępnym podejściu `async/await`.
### Modele
W plikach `enums.py` umieszczono enumeracje z definicjami wszystkich statusów oraz nagłówków wykorzystywanych w protokole. Zdefiniowano również struktury pomocniczne, np.: `HeadersContainer` do obsługi i walidacji nagłówków.
Główne klasy to `Request` oraz `Response`, wspierające modułową serializację i deserializację, co widać np. w rozszerzonej klasie `FileResponse`, zawierającej całą logikę do wysyłania lokalnego zasobu jako zawartość `Response`.
### Parsowanie nagłówków
Korzystając z licznych uproszczeń wynikających ze wsparcia protokołów tekstowych w `StreamReader`, zdefiniowanych enumeracji oraz funkcji walidujących, jedyną trudnością okazały się nagłówki o bardziej skomplikowanej formie, do czego wykorzystano proste wyrażenia regularne, np. `Range: (\S+) (\d*)-(\d*)`. Pojedyncze metody pomocniczne do przetwarzania tekstu znajdują się w `parse_utils.py`.
### Logika
Zarówno dla serwera i klienta protokołu, `asyncio` opakowuje gniazdo dostarczając wysokopoziomowe klasy `StreamReader`/`StreamWriter` do odczytu i zapisu danych ze strumienia. W plikach odpowiednio `client.py` oraz `server.py` znajdują się klasy, których instancje obsługują cały cykl życia pojedynczego połączenia pomiędzy klientem i serwerem. W obu przypadkach logika ogranicza się do pojedynczej funkcji obsługi zdeserializowanej klasy `Request` bądź `Response`. Zastosowanie własnych klas wyjątków pozwala na łatwą propagację błędów i zwrócenie odpowiedniego kodu błędu lub zamknięcie połączenia.
### Komunikacja z resztą programu
W celu obsłużenia zapytania o plik należy pobrać z głównego kontrolera aktualny stan pliku, udostępniając możliwość synchronizacji stanu jednocześnie zachowując kontrolę nad dostępem do pliku. W tym celu, w `context.py` zdefiniowano 'konteksty' `FileProviderContext`/`FileConsumerContext` pośredniczące w 'transakcji' dotyczącej pojedynczego pliku. Są to klasy implementujące wzorzec menedżera kontekstu *(powszechnie stosowane w konstrukcji `with`)*, zwalniane w momencie zaprzestania korzystania z powiązanego zasobu. Kontroler przetrzymuje dla każdego pliku listę konsumentów *(przy udostępnianiu pliku)* oraz dostawcę *(w przypadku pobierania pliku)*.
## 6.3 Repozytorium plików
### Domyślna struktura katalogów
```
/home/user/
+--Downloads
| +-- simplep2p
| +-- .meta
| | +-- added_file1.txt.yaml
| | +-- downloaded_file1.yaml
| | +-- downloaded_file2.yaml
| +-- downloaded_file1
| +-- downloaded_file2
```
### Pliki z metadanymi
Informacje o każdym pliku dodanym do repozytorium gromadzone są w katalogu `.meta` ukazanej wyżej struktury. Zawierają one w sobie informacje takie jak: nazwa pliku, ścieżka bezwzględna pliku, rozmiar w bajtach w momencie dodawania, status, wyznaczony skrót w momencie dodawania, obecny rozmiar oraz obecny skrót.
<small>Przykład pliku z metadanymi</small>
```
current_digest: 4bc89cbe78f14ccecef1571aa46048b770353c33a0000e304c2c2e4610a8b853
current_size: 950
digest: 4bc89cbe78f14ccecef1571aa46048b770353c33a0000e304c2c2e4610a8b853
name: added_file1.txt
path: /home/username/Desktop/added_file.txt
size: 950
status: ready
```
W przypadku, kiedy użytkownik zmodyfikował plik poza naszą aplikacją, zostanie wyliczony dla niego nowy skrót oraz rozmiar. Następnie zastąpi on poprzednie wartości w polach z nazwą "current_".
Możliwe statusy pliku:
- `ready` - plik jest w pełni gotowy, obecność pliku jest obecnie rozgłaszana innym klientom w sieci
- `downloading` - plik jest obecnie pobierany
- `invalid` - plik został zmodyfikowany przez użytkownika poza aplikacją
W przypadku przerwania pobierania (np. nagłe zatrzymanie działania aplikacji), pobieranie wznowi się po ponownym uruchomieniu na podstawie bieżącego rozmiaru pliku.
Jeśli użytkownik chce kontynuować rozgłaszanie pliku ze statusem `invalid`, musi on zostać dodany ponownie.
## 6.4 Główny kontroler
### Zarządzanie komponentami
Kontroler zarządza cyklem życia modułów: repozytorium, UDP oraz serwera plików. Tworzona jest pętla obsługi zdarzeń `asyncio` dla serwera TCP oraz periodycznego obserwowania stanu plików. Wszystkie komponenty komunikują się między sobą za pośrednictwem kontrolera; synchronizacja odbywa się za pomocą semafor z pakietu `threading`.
### Obserwacja stanów plików
Co 5 sekund badane są stany plików *(w celu wykrycia nieprawidłowych plików)* oraz podejmowane są próby wznowienia pobierania.
### Przechowywanie stanów
Nie wszystkie dane o plikach muszą być na bieżąco utrwalane w repozytorium (np. skrót oraz rozmiar pliku), stąd w kontrolerze przechowywana jest kopia słownika plików synchronizowana ze stanem repozytorium.
## 6.5 Konfiguracja
Podstawowa konfiguracja, na którą składają się stałe takie jak rozmiary buforów czy maksymalne rozmiary plików, zawiera się w pliku `common/config.py`. W pliku `log.yml` znajduje się konfiguracja loggerów, dzięki czemu istnieje możliwość ustawienia odpowiedniego poziomu logowania.
```python
# fragment pliku common/config.py
#...
BROADCAST_OMIT_SELF = True
PROTO_VERSION = 1
ENCODING = "utf-8"
MAGIC_NUMBER = 0xD16D
UDP_BUFFER_SIZE = 2048
UDP_PEER_CLEANUP_PERIOD = 30
UDP_ADVERTISE_PERIOD = 10
TCP_FILE_SEND_TIMEOUT = 15
TCP_FILE_RECEIVE_TIMEOUT = 10
FILE_WATCHER_PERIOD = 5
MAX_FILENAME_LENGTH = 32
DIGEST_ALG = "sha256"
FINDING_TIME = 2
SEARCH_RETRIES = 2
#...
```
```yaml
# fragment pliku log.yml
formatters:
request:
format: '%(asctime)s | %(name)s | %(id).8s | %(levelname)s | %(method)s %(uri)s:
%(message)s'
simple:
format: '%(asctime)s | %(name)s | %(levelname)s | %(message)s'
handlers:
request-console:
class: logging.StreamHandler
formatter: request
level: DEBUG
stream: ext://sys.stderr
loggers:
ClientHandler:
handlers:
- request-console
level: DEBUG
```
Wszystkie najważniejsze opcje można przekazać jako argument wywołania programu *(są wczytywane do singletona `Config`)*:
```
Simple P2P client
optional arguments:
-h, --help show this help message and exit
--bind-ip BIND_IP IP to bind for file transfer and discovery (default: 0.0.0.0)
--tcp-port TCP_PORT TCP port to use for file transfer (default: 13372)
--udp-port UDP_PORT UDP port to use for file discovery (default: 13371)
--broadcast-iface BROADCAST_IFACE
Interface to broadcast on for peer/file discovery, eg. eth0 (default: default)
--broadcast-port BROADCAST_PORT
UDP port to broadcast on for peer/file discovery (default: 13370)
--broadcast-drop-chance BROADCAST_DROP_CHANCE
Percentage chance to drop incoming broadcast packet (default: 0)
--broadcast-drop-in-row BROADCAST_DROP_IN_ROW
Number of packets to be dropped at once (default: 1)
```
## 6.6 Interfejs użytkownika
Interfejs użytkownika to interaktywny wiersz poleceń wywołujący metody kontrolera, zgodnie z funkcjonalnością zawartą w paragrafie nr **3.**
## 6.7 Logowanie
Wykorzystano wielopoziomowe logowanie z podziałem na moduły; ważniejsze komunikaty wyświetlane są w konsoli, ponadto bardziej szczegółowe dane zapisywane są do pliku tekstowego. Logowane są wszystkie nieobsłużone oraz niespodziewane wyjątki, sytuacje nietypowe i informacje o komunikacji przepływającej przez protokoły UDP/TCP oraz kontroler.
Dla każdego połączenia TCP generowany i logowany jest jego unikalny identyfikator.
# 7. Testy oraz obsługa sytuacji wyjątkowych
Nieistotne fragmenty wyjścia z konsoli pominięte w celu zwiększenia czytelności.
## 7.1 Wariant W21 - Gubienie datagramów rozgłaszanych
Przykład został przygotowany poprzez jednoczesne uruchomienie aplikacji na dwóch komputerach w ramach tej samej sieci fizycznej. Została włączona programowa symulacja 'gubienia' pakietów za pomocą argumentów `--broadcast-drop-chance 100 --broadcast-drop-in-row 3`.
### Przykłady (nie)działania:
Widok konsoli hosta `10.1.1.10`:
```
23:03:31|UdpController|DEBUG|Hello|Responding with HERE to new peer 10.1.1.7
23:03:31|UdpController|DEBUG|Here|Received HERE message from peer 10.1.1.7:12345
23:03:31|UdpController|DEBUG|Here|Discovered peer 10.1.1.7:12345
23:03:39|UdpController|DEBUG|Broadcasting HERE message
23:03:41|UdpController|DEBUG|Here|Received HERE message from peer 10.1.1.7:12345
> search asd
Searching... please wait
23:03:49|UdpController|DEBUG|Broadcasting HERE message
23:03:51|UdpController|DEBUG|Here|Received HERE message from peer 10.1.1.7:12345
23:03:53|UdpController|INFO|Search|1 peers did not respond, retrying search for file asd (1/2)
23:03:55|UdpController|INFO|Search|1 peers did not respond, retrying search for file asd (2/2)
23:03:57|UdpController|INFO|Search|Deleting unresponsive peer 10.1.1.7
23:03:57|UdpController|INFO|Search|Found asd in 0 out of 1 peers
No files were found in the network
```
Widok konsoli hosta `10.1.1.7`:
```
Starting... this might take a bit
23:03:31|Controller|INFO|Starting...
23:03:31|UdpController|DEBUG|Broadcasting HERE message
23:03:31|AsyncioDatagramProtocol|DEBUG|Dropping incoming broadcast datagram (1/3)
23:03:31|Controller|INFO|Ready
Welcome to SimpleP2P! Type ? to list commands
>
23:03:39|AsyncioDatagramProtocol|DEBUG|Dropping incoming broadcast datagram (2/3)
23:03:41|UdpController|DEBUG|Broadcasting HERE message
23:03:49|AsyncioDatagramProtocol|DEBUG|Dropping incoming broadcast datagram (3/3)
23:03:51|UdpController|DEBUG|Find|Received datagram from unknown host 10.1.1.10, skipping
23:03:51|UdpController|DEBUG|Broadcasting HERE message
23:03:53|AsyncioDatagramProtocol|DEBUG|Dropping incoming broadcast datagram (1/3)
23:03:55|AsyncioDatagramProtocol|DEBUG|Dropping incoming broadcast datagram (2/3)
23:03:59|AsyncioDatagramProtocol|DEBUG|Dropping incoming broadcast datagram (3/3)
23:04:01|UdpController|DEBUG|Broadcasting HERE message
```
### Opis
- 23:03:31 - `10.1.1.7` włączył program SimpleP2P i wysłał datagram `HELLO`, na który host `10.1.1.10` odpowiedział datagramem `HERE`
- `10.1.1.7` wszedł od razu w tryb gubienia pakietów, dlatego nie odebrał od niego odpowiedzi HERE
- Tymczasem, host `10.1.1.10` poprawnie odebrał datagram `HERE` od `10.1.1.7` i dodał go do swojej listy partnerów
- 23:03:39 - `10.1.1.7` zgubił datagram `HERE` od `10.1.1.10`, dlatego wciąż nie wie o istnieniu `10.1.1.10`
- 23:03:41 - `10.1.1.10` poprawnie odebrał datagram `HERE` od `10.1.1.7`
- 23:03:49 - `10.1.1.7` ponownie zgubił datagram `HERE` od `10.1.1.10`
- 23:03:51 - `10.1.1.7` otrzymał datagram `FIND` od `10.1.1.10`, ale nie miał go na liście partnerów, dlatego mu nie odpowiedział (nie ma informacji np. na jaki port unicast ma odpowiadać)
- 23:03:53 - 2 sekundy później, `10.1.1.10` ponowił szukanie pliku "asd", ale `10.1.1.7` znowu zaczął odrzucać pakiety
- 23:03:57 - Po trzech nieudanych próbach, `10.1.1.10` usunął `10.1.1.7` z listy partnerów przez brak odpowiedzi
## 7.2 Istnienie wielu wersji tego samego pliku
Nasz program w pełni wspiera sytuację, kiedy istnieje kilka wersji pliku identyfikowanego przez tą samą nazwę. Nazwy plików nadal są unikalne w skali lokalnego repozytorium, lecz użytkownik może wybrać w takiej sytuacji wersję na podstawie skrótu. Nawet w przypadku kontynuowania pobierania, wyszukiwany będzie plik ze zgodnym skrótem.
##### Przykład działania z plikiem o dwóch wersjach
```
Welcome to SimpleP2P. Type ? to list commands
> download hello.txt
Searching... please wait
Files found in the network:
+----+-----------+-------------+------+---------+
| ID | Name | Fingerprint | Size | From |
+----+-----------+-------------+------+---------+
| 0 | hello.txt | 50660cced7 | 26 | 1 peers |
| 1 | hello.txt | 8b9040011c | 4 | 1 peers |
+----+-----------+-------------+------+---------+
Found multiple versions. Please choose one.
Select provider index: 0
Starting download...
2022-01-18 22:04:24,379 | Controller | INFO | Download of hello.txt completed
> status
+----+-----------+-------------+------+--------+----------+---------+
| ID | File name | Fingerprint | Size | Status | Progress | Peer(s) |
+----+-----------+-------------+------+--------+----------+---------+
| 0 | hello.txt | 50660cced7 | 26 | READY | --- | --- |
+----+-----------+-------------+------+--------+----------+---------+
```
## 7.3 Błąd sieci podczas pobierania
Poniższy test został przeprowadzony pomiędzy hostem `192.168.80.1` oraz systemem zwirtualizowanym `192.168.80.132`. Podczas pobierania pliku od hosta do gościa, interfejs sieciowy odbiorcy został nagle wyłączony i ponownie włączony po ok. 30 sekundach.
#### Stan początkowy
##### Nadawca pliku
```
13:59:05,916 | ServerHandler | c701e2cd | DEBUG | : New connection from 192.168.80.132:46538
13:59:05,917 | ServerHandler | c701e2cd | INFO | GET video.mkv: Response code=200 len=3987561655
```
##### Odbiorca pliku
```
13:59:05,591 | Controller | INFO | Starting download of file video.mkv from 192.168.80.1
13:59:05,592 | ClientHandler | f7802949 | DEBUG | GET video.mkv: New connection to 192.168.80.1:13372
> status
+----+-----------+-------------+------------+-------------+----------+--------------+
| ID | File name | Fingerprint | Size | Status | Progress | Peer(s) |
+----+-----------+-------------+------------+-------------+----------+--------------+
| 0 | video.mkv | a48dcfffaf | 3987561655 | DOWNLOADING | 1.30% | 192.168.80.1 |
+----+-----------+-------------+------------+-------------+----------+--------------+
```
#### Stan po odłączeniu i ponownym podłączeniu intefejsu sieciowego
##### Nadawca pliku
```
13:59:45,411 | ServerHandler | c701e2cd | ERROR | GET video.mkv: Connection error
[...]
14:00:02,736 | UdpController | DEBUG | Find | Sending positive reply for file video.mkv with digest a48dcfff
14:00:04,736 | ServerHandler | 796a29e4 | DEBUG | : New connection from 192.168.80.132:46540
14:00:04,737 | ServerHandler | 796a29e4 | INFO | GET video.mkv: Response code=206 len=3886426695
```
##### Odbiorca pliku
```
13:59:37,400 | UdpController | ERROR | AliveAgent | Error while broadcasting HERE
13:59:40,124 | Controller | WARNING | Download of video.mkv failed
13:59:52,414 | UdpController | ERROR | Search | Error while broadcasting
OSError: [Errno 101] Network is unreachable
13:59:57,418 | Controller | INFO | Retrying file video.mkv
13:59:57,418 | UdpController | DEBUG | AliveAgent | Broadcasting HERE message
13:59:59,421 | Controller | WARNING | Cannot find hosts to resume file video.mkv
14:00:01,105 | UdpController | DEBUG | Here | Discovered peer 192.168.80.1:13370
14:00:02,422 | Controller | INFO | Retrying file video.mkv
14:00:04,425 | UdpController | DEBUG | Found | Found file video.mkv
14:00:04,227 | Controller | INFO | Starting download of file video.mkv from 192.168.80.1
14:00:04,425 | ClientHandler | 89e91323 | DEBUG | GET video.mkv: New connection to 192.168.80.1:13372
> status
+----+-----------+-------------+------------+-------------+----------+--------------+
| ID | File name | Fingerprint | Size | Status | Progress | Peer(s) |
+----+-----------+-------------+------------+-------------+----------+--------------+
| 0 | video.mkv | a48dcfffaf | 3987561655 | DOWNLOADING | 3.57% | 192.168.80.1 |
+----+-----------+-------------+------------+-------------+----------+--------------+
```
* Interfejs został wyłączony w momencie ok. `13:59:30`. Od tego momentu wszystkie próby rozgłaszania datagramów zwróciły błąd `Network unreachable`.
* Transfer pliku zwrócił błąd związany z przekroczeniem czasu oczekiwania, który został określony w konfiguracji jako 10 sekund w przypadku klienta oraz 15 sekund w przypadku serwera.
* Odbiorca pliku próbował ponowić pobieranie pliku w momencie `13:59:57`, lecz nie znalazł partnera.
* Intefejs sieciowy odbiorcy został ponownie podłączony w momencie ok.`14:00:00`; wkrótce wykryto ponownie partnera `192.168.80.1`.
* Odbiorca pliku podjął kolejną, tym razem udaną próbę wznowienia pobierania pliku.
* Transfer pliku został wznowiony od momentu przerwania, o czym świadczy kod odpowiedzi `206 Partial content` z krótszą zawartością.
## 7.4 Nadawca przesyłający niepoprawną wersję pliku
Nasz uproszczony schemat działania repozytorium weryfikuje skrót pliku jednorazowo przy uruchomieniu programu oraz po zakończeniu pobierania; istnieje więc możliwość, że serwer nada zmodyfikowaną wersję pliku w przypadku np. 'podmiany' pliku przez użytkownika. Optymalnym rozwiązaniem byłoby blokowanie dostępu do pliku oraz badanie dat modyfikacji, lecz uznaliśmy, że implementacja wykracza poza zakres projektu.
Poniższy test został przeprowadzony pomiędzy hostem `192.168.80.1` oraz systemem zwirtualizowanym `192.168.80.132`. Po wczytaniu listy plików, zmodyfikowano plik `video.mkv` dopisując kilka dodatkowych bajtów.
#### Odbiorca pliku
```
15:03:30,773 | Controller | INFO | Starting download of file video.mkv from 192.168.80.1
15:03:30,773 | ClientHandler | 28440edb | DEBUG | GET video.mkv: New connection to 192.168.80.1:13372
[...]
15:04:20,728 | Controller | INFO | Download of video.mkv completed
15:04:20,729 | Controller | WARNING | Download of video.mkv failed
simple_p2p.common.exceptions.LogicError: Invalid file download
15:04:24,933 | Controller | WARNING | Truncating download video.mkv
15:04:24,934 | Controller | INFO | Retrying file video.mkv
15:04:24,937 | UdpController | WARNING | Found | Received FoundDatagram from unknown peer 192.168.80.1
15:04:24,967 | UdpController | DEBUG | AliveAgent | Broadcasting HERE message
15:04:25,482 | UdpController | DEBUG | Here | Received HERE message from peer 192.168.80.1:13370
15:04:25,482 | UdpController | DEBUG | Here | Discovered peer 192.168.80.1:13370
15:04:26,937 | UdpController | INFO | Search | Found video.mkv in 0 out of 0 peers
15:04:26,937 | Controller | WARNING | Cannot find hosts to resume file video.mkv
```
* Plik został pomyślnie pobrany, po czym nastąpiło porównanie skrótu pliku pobranego z tym spodziewanym i wykryto niepoprawność
* Nadawca pliku został tymczasowo usunięty z listy partnerów
* Nieoczekiwany plik został usunięty - pobieranie należy kontynuować od zera
* Nastąpiła próba ponowienia pobierania, lecz nie udało się znaleźć partnera
Niestety, w naszym podejściu serwer nie potrafi wykryć faktu przesłania nieodpowiedniego pliku, więc nadal będzie udostępniał nieprawidłowy plik. Wyjątkiem jest próba przesłania pliku, który nie istnieje bądź ma rozmiar mniejszy niż spodziewany - w takim wypadku kontroler wykrywa błąd i ustawia stan pliku na `invalid`.
W celu poprawnego obsłużenia tego scenariusza, należałoby zastosować jedno z rozwiązań zaproponowanych na początku opisu testu.
# 8. Instrukcja instalacji
Do poprawnego działania programu wymagany jest interpreter Python w wersji co najmniej `3.9`
```bash
$ python3.9 -m venv venv && source venv/bin/activate
$ pip3 install -r requirements.txt && pip3 install .
$ simple-p2p
```
### Dziennik zmian w raporcie na tle pierwszego etapu:
> 1. Interpretacja zadania
> Identyfikator zasobu jest teraz nazwą pliku, a nie 64-bajtowym skrótem
> 2. Opis funkcjonalny
> Po pobraniu, plik nie jest usuwany, można wznowić pobieranie
> Interfejs konsolowy nie pozwala na przerwanie uploadowania bez usunięcia pliku z repozytorium (`stop-upload`)
> Aktualizacja wyglądu i innych funkcjonalności interfejsu tekstowego
> 3.1 Opis stosowanych protokołów
> UDP
- Zmiana kolejności nazwy/skrótu pliku w `struct msg_file_data`
- Dodanie pola file_size w `struct msg_file_data`
- Zmiana nazw niektórych pól
- Dodanie pola `unicast_port` w `struct msg_here`
> TCP
- Dodanie statusu `416 Invalid Range`
- Dodanie nagłówka odpowiedzi `Range-content`
- Połączenie jest zamykane po wysłaniu odpowiedzi
>3.2 Analiza stosowanych protokołów
> Usunięto `PORT 1234` z datagramu `HERE`
> 4. Podział na moduły i komunikacja
> Zmieniono tytuł na 'Schemat komunikacji węzłów'
> 5. Zarys koncepcji implementacji
> Zmieniono tytuł na 'Wykorzystane narzędzia'
> Dodanie zewnętrznych bibliotek
> Uwzględnienie `asyncio` oraz logowania
> 6. Implementacja
> Dodanie paragrafu wraz z opisami poszczególnych modułów
> 7. Testy
> Dodanie paragrafu wraz z opisem realizacji wariantu W21