Category Archives: Ogólne

Fuslogvw a assembly binding

Czasami proste debugowanie za pomocą Visual Studio nie wystarcza. Sposób w jaki biblioteki są ładowane zależy od wielu czynników i plików konfiguracyjnych. Jeśli np. nasza aplikacja wyrzuca wyjątek “MissingMethodException”, a w czasie debugowania wszystko działa jak należy, wtedy możliwe, że mamy problemy związane z assembly binding.

Narzędzie Fuslogvw wchodzi w skład Visual Studio i pozwala prześledzić jakie kolejno biblioteki ładowane są. Dzięki temu poznamy ich lokalizacje oraz wersje.

W celu uruchomienia fuslogvw, odpalamy najpierw wiersz poleceń Developer Command Propmpt (koniecznie jako administrator):
1Następnie wpisujemy “Fuslogvw.exe”, co spowoduje uruchomienie narzędzia:
2

W “Settings” możemy ustawić czy chcemy wyłącznie logować błędy czy wszystkie wiązania:
3

Następnie stwórzmy jakaś aplikację (np. ConsoleApp) w celu zobaczenia co zaloguje Fuslogvw. Po odpaleniu przykładowej aplikacji naciskamy “Refresh” w Fuslogvw:
4

Na ekranie zobaczymy kilka nowych wpisów. Klikając na jedno z nich przejdziemy do szczegółów:
5

W szczegółach widzimy wyraźnie jaka biblioteka jest ładowana i skąd. Z kolei jeśli jakieś biblioteki nie można załadować, wtedy logi będą wyglądać następująco:
6

Na screenshocie można zauważyć, które  ścieżki są kolejno brane pod uwagę, gdy pliku dll nie ma w głównym katalogu aplikacji.

.NET Core oraz ASP.NET Core

.NET Core to nowy framework od Microsoft’u, który aktualnie wciąż jest w produkcji. Zdecydowałem się napisać dzisiejszy post, ponieważ w ostatnim czasie nastąpiło wiele zmian w wersjonowaniu i nazewnictwie .NET. Moim zdaniem wprowadziło to trochę zamieszania.

Przede wszystkim, co to jest .NET Core? To nowa wersja framework’a, która z założenia ma być wieloplatformowa oraz open-source. To wielka zmiana ze strony Microsoft, ale już od kilku lat można obserwować, że Microsoft zmierzą w kierunku, który w latach 90 był kompletnie nie do pomyślenia. .NET Core będzie zatem działać zarówno na Windows jak i Mac czy Linux.

Druga różnica to fakt, że .NET Core to po prostu pakiet NuGet. Doskonale nadaje się to do aplikacji webowych. Dzisiaj, serwer web musi mieć zainstalowany .NET Framework. Oznacza to, że ciężko mieć kilka aplikacji ASP.NET uruchomianych w różnych wersjach .NET Framework.  Dzisiejsza wersja .NET Framework jest bardzo mocno powiązana z systemem operacyjnym i z punktu architektonicznego stanowi monolit.

Wchodząc trochę w szczegóły techniczne, .NET Core składa się z podstawowej biblioteki klas (CoreFx), oraz środowiska uruchomieniowego CoreClr. Na GitHub można śledzić postęp prac:

https://github.com/dotnet/corefx

https://github.com/dotnet/coreCLR

Ktoś może zapytać się, ale co z Mono? Aktualnie jest to implementacja .NET na Linuxa. Po wprowadzeniu .NET Core nie jest jeszcze jasne co stanie się z Mono, ponieważ jest to niezależny framework. Na dzień dzisiejszy, wydaje mi się, że w pewnym czasie .NET Core zastąpi całkowicie Mono.

Wprowadzenie .NET Core nie oznacza, że nagle będziemy mogli uruchamiać wszystkie aplikacje na Linux. .NET Core to jedynie podzbiór .NET Framework. Wiadomo, że główną motywacją były aplikacje webowe czyli ASP.NET. Inne typy aplikacji typu WPF nie będą oczywiście dostępne (przynajmniej bazując na informacjach, które są już dostępne).

Nie trudno teraz domyślić się, że ASP.NET Core to nowa wersja ASP.NET,  napisana pod .NET Core. Oznacza to, że będzie mogłaby być hostowana na Linux. Analogicznie do .NET Core, ASP.NET Core jest również open-source.

ASP.NET 5, o którym pisałem wiele razy na blogu, został zmieniony na ASP.NET Core. Informacje, które wcześniej podawałem,  dotyczą zatem ASP.NET Core, a nie ASP.NET 5, którego nazwa została zmieniona.  Jedną z większych zmian ASP.NET Core to wprowadzenie projects.json, zamiast packages.config. Więcej szczegółów można znaleźć w poprzednich wpisach.

Podsumowując, .NET Core oraz ASP.NET Core dadzą nam:

  • Wieloplatformowość
  • Modularność i niezależność od .NET Framework zainstalowanego na systemie – każda aplikacja, będzie mogą korzystać z innej wersji framework’a.
  • Open-source

Warto również przejrzyj się następującemu diagramowi (źródło  http://www.hanselman.com):

Jak widzimy, ASP.NET Core 1.0, będzie działać również na starym, monolitowym frameworku (.NET Framework 4.6). Taka konfiguracja też jest możliwa tzn. ASP.NET Core  + .NET Framework, ale w celu uzyskania wieloplatformowości należy zainteresować się ASP.NET Core 1.0 + .NET Core.

Wirtualne maszyny a kontenery

Za sprawą Docker, coraz bardziej popularnym terminem staje się “software container”. Jak to bywa z nowikami ostatnio, czasami są one wdrażane na siłę, w środowiskach gdzie nie ma takiej potrzeby.

Na blogu skupiam się na środowisku Windows, dlatego również o kontenerach będę pisał od strony programistów Windows. Najpierw jednak wypada wyjaśnić co to jest kontener?

Wiele lat temu, standardem były fizyczne maszyny. Gwarantowało to oczywiście 100% izolacji między aplikacjami. Aplikacja na komputerze A, nie mogła bezpośrednio adresować pamięci na komputerze B. Z punktu widzenia bezpieczeństwa osiągano w ten sposób wysoką niezależność. Niestety, w momencie gdy chcieliśmy wdrążyć drugi serwis\aplikację, wtedy stawało się to bardzo kosztowne ponieważ musieliśmy zakupić dodatkowy serwer. Można to pokazać następująco:

1

Ogromnym przełomem były wirtualne maszyny. Dzięki nim, na tym samym komputerze, mogliśmy symulować kilka innych systemów operacyjnych. Co ważne, każdy z nich miał swoje zasoby takie jak CPU czy pamieć podręczna. Z punktu widzenia izolacji i bezpieczeństwa, rozwiązanie było znakomite.

2

Nastały jednak czasy, gdzie taka skalowalność nie wystarczała. Wyobraźmy sobie architekturę opartą o mikro-serwisy. Przy dużym ruchu chcemy mieć możliwość aktywowania pewnych węzłów jak szybko tylko to możliwe. Wirtualne maszyny to dosyć ciężkie rozwiązanie. Każda maszyna to osobny system operacyjny. Każdy system z kolei pożera na starcie masę zasobów (CPU, pamieć). Instalacja maszyny również jest dość powolna.

Z pomocą przychodzą kontenery. Można je określić mianem wewnętrznej wirtualizacji. Pojedynczy system zawiera kilka kontenerów, które poziomem izolacji przypominają wirtualne maszyny:

3

Dzięki kontenerom nie mamy overhead, który wiązał się z wirtualną maszyną. Uruchomienie nowego kontenera jest również dużo szybsze niż VM. W wielu wypadkach takie rozwiązanie jest optymalne ponieważ zwykle nie potrzebujemy osobnego systemu, a chodzi nam wyłącznie o izolację. W przypadku mikro-serwisów, kontenery są dobrym rozwiązaniem ponieważ nie potrzebujemy osobnych systemów operacyjnych – wystarczy jeden, wspierający kontenery.

DbUp – aktualizacja baz danych

DbUp jest prostą biblioteką, przeznaczoną do aktualizacji baz danych. Jeśli korzystamy z ORM, zwykle wtedy dany framework posiada już analogiczną funkcjonalność. Na przykład, EntityFramework wspiera migrację, która umożliwia automatyczną aktualizacje tabel i procedur.

Z drugiej strony, nie zawsze jest potrzeba korzystania z tak ciężkich rozwiązań. Bardzo popularną biblioteką do odczytu danych z baz jest Dapper.  Niestety nie posiada on mechanizmu podobnego do Entity Framework migrations.

W takich przypadkach, DbUp jest bardzo przydatny. Aktualnie wspiera kilka baz danych m.in. SQL Server, MySQL i FireBird. Idea jest bardzo prosta – wystarczy w projekcie stworzyć folder z listą skryptów do wykonania.  Załóżmy, że mamy 2 skrypty :

CREATE TABLE [dbo].Persons
(
   [Id] INT NOT NULL PRIMARY KEY, 
    [FirstName] NCHAR(10) NULL, 
    [LastName] NCHAR(10) NULL
)

CREATE TABLE [dbo].Articles
(
	[Id] INT NOT NULL PRIMARY KEY, 
    [Title] NCHAR(10) NULL, 
    [Content] NCHAR(500) NULL
)

Każdy ze skryptów powinien zostać zapisany w osobnym pliku, np.:

1

Następnie należy ustawić Build Action na Embedded Resource:

2
Oczywiście jeśli chcemy, aby DbUp wykonał powyższe skrypty, należy zainstalować odpowiedni pakiet za pomocą:
Install-Package DbUp

W celu uruchomienia aktualizacji wystarczy:

            var connectionString = @"Server=DESKTOP-IK0BKOF\SQLEXPRESS;database=testdb; Trusted_connection=true";

            var upgrader =
                DeployChanges.To
                    .SqlDatabase(connectionString)
                    .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
                    .LogToConsole()
                    .Build();

            var result = upgrader.PerformUpgrade();

            if (!result.Successful)
                Console.WriteLine(result.Error);

DbUp automatycznie połączy się z bazą i wykona skrypt po skrypcie:

3

Jeśli ponownie spróbujemy wykonać aktualizację, wtedy DbUp zorientuje się, że nie trzeba już żadnych skryptów wykonywać:

4

Załóżmy, że po pewnym czasie dodaliśmy kolejną tabelę:

CREATE TABLE [dbo].Contacts
(
	[Id] INT NOT NULL PRIMARY KEY, 
    [FirstName] NCHAR(10) NULL, 
    [LastName] NCHAR(10) NULL
)

5
Po uruchomieniu, DbUp doda wyłącznie nową tabelę (Contacts):

6

Pozostaje wyjaśnij, w jaki sposób DbUp wie, które skrypty należy wykonywać? Zaglądając do bazy danych, zobaczymy, że nowa tabela “SchemaVersions” została utworzona. Wykonując Select na niej, przekonamy się,  że DbUp przechowuje listę skryptów, które zostały wykonane:

7

Oprócz aplikacji konsolowej, można korzystać również ze skryptów PowerShell (szczegóły w dokumentacji). Innym, częstym podejściem jest wykonywanie aktualizacji przed uruchomieniem aplikacji.

Jak widać, DbUp to bardzo proste narzędzie, wykonujące po prostu listę skryptów. Przydaje się jednak w częstych releasach,  gdzie należy pamiętać historię zmian w DB.

Exceptionless – centralne przechowywanie logów

Standardowo w aplikacjach używa się takich frameworków jak log4net czy nlog w celu logowania kluczowych informacji jak i  wyjątków. Bardzo szybko staje się jasne (szczególnie w przypadku micro-serwisów), że analizowanie plików tekstowych z logami jest czasochłonne.

Z tego względu dobrze mieć centralne repozytorium logów i łatwy do niego dostęp. Większość rozwiązań umożliwia dzisiaj indeksowanie oraz łatwe przeszukiwanie danych. Jednym z bardziej znanych produktów jest splunk. Umożliwia agregację logów z różnych źródeł i szybkie wyszukiwanie za pomocą zaawansowanych zapytań.  Za pomocą splunk łatwe jest wygenerowanie raportów np. pokazujących jaki typ wyjątku był najczęściej wyrzucany w określonym czasie.  Niestety rozwiązanie jest dość drogie.

Exceptionless nie jest alternatywą dla splunk. Jeśli jednak potrzebujemy wyłącznie centralnego repozytorium logów z możliwością wykonania prostych zapytań, wtedy ExceptionLess jest tanią lub nawet darmową alternatywą. ExceptionLess jest open-source, można zatem pobrać kod źródłowy i bez problemu hostować to na wewnętrznym serwerze. Możliwe jest również hostowanie na ich serwerach i wtedy cena zależy od liczby projektów i innych parametrów.  Dla testów jednak (jeden projekt), można stworzyć darmowe konto.

Spróbujmy zatem napisać proste demo. Zaczynamy od stworzenia darmowego konta na https://be.exceptionless.io/signup. Następnie tworzymy nowy projekt:

1

Po wybraniu typu projektu (ASP.NET MVC), dostaniemy informacje, co należy wykonać dalej:

2

Jak widzimy, musimy zainstalować odpowiedni pakiet za pomocą Install-Package Exceptionless.Mvc. Ze screenu wynika również, że musimy ustawić APIKey reprezentujący projekt, który właśnie stworzyliśmy. Wystarczy ustawić odpowiedni element w Web.config

  <exceptionless apiKey="API_KEY_HERE" />

I to naprawdę wszystko! Przyglądając się Web.config zobaczymy również, że dodatkowy moduł został dołączony:

  <system.webServer>
    <validation validateIntegratedModeConfiguration="false" />
    <modules>
      <add name="ExceptionlessModule" type="Exceptionless.Mvc.ExceptionlessModule, Exceptionless.Mvc" />
    </modules>
  </system.webServer>

Dzięki temu, nieobsłużone wyjątki zostaną zapisane automatycznie w bazie ExceptionLess. Spróbujmy zatem wyrzucić jakiś wyjątek w kontrolerze:

        public ActionResult Index()
        {
            throw new ArgumentNullException("Any message");

            return View();
        }

Przechodząc teraz do zakładki “Exceptions”, zobaczymy, że wyjątek został przechwycony przez ExceptionLess i zapisany:

3

Klikając na wpisie, przejdziemy do szczegółów:

4

U góry aplikacji widzimy również opcje wyszukiwania, która wspiera m.in. znak “*”:

5

Dane są indeksowane, zatem wyszukiwanie jest szybkie.

W ustawieniach konta, możemy również skonfigurować powiadomienia o np. krytycznych wyjątkach lub otrzymywać codzienny raport. ExceptionLess nie służy wyłącznie do przechowywania wyjątków, ale do logowania jakichkolwiek zdarzeń. Do każdego zdarzenia można dołączać tag, dzięki temu exceptionless jest przydatny w monitorowaniu działania aplikacji. Za pomocą raportów, w łatwy sposób możemy dowiedzieć się, które elementy aplikacji są najczęściej wykorzystywane.

Blue Green Release

Proces releasowania oprogramowania jest chyba jednym z ważniejszych wyzwać w czasach oprogramowania bazującego na usługach. 20 lat temu,  oprogramowanie zwykle było sprzedawane na nośnikach danych i częstotliwość wydawania nowych wersji była bardzo niska (np. dwie aktualizacje na rok).

Myślę, że 5-10 lat temu, standardem stało się releasowanie co iterację, np. raz na dwa tygodnie.  Od kilku lat jednak, wiele firm wdraża zmiany kilkakrotnie dziennie i taki proces można dopiero nazwać “continuous deployment”.

Temat bardzo szeroki, ale dzisiaj chciałbym przyjrzeć się tylko wzorcowi blue-green. Wiemy, że wdrożenie nowego kodu jest bardzo ryzykowne, nawet gdy mamy dobre pokrycie testami. W praktyce, istnieje wiele innych problemów takich jak konfiguracja zapory ogniowej czy odpowiednie uprawnienia.  Skoro chcemy wdrażać często, nie jesteśmy w stanie sobie pozwolić na jakąkolwiek przerwę w działaniu systemu.

Wzorzec blue-green zakłada, że mamy do dyspozycji przynajmniej dwa serwery: blue oraz green. W dowolnym momencie tylko jeden z nich jest aktywny oraz posiada najnowszą wersję. Ponadto do dyspozycji powinniśmy mieć load balancer albo router, który będzie przełączał przekierowania do serwera blue albo green. Na początku oczywiście, przy pierwszym wdrożeniu nie ma znaczenia, który z serwerów wybierzemy. Sytuacja zatem może wyglądać następująco:

1

Blue zawiera pierwszą wersje kodu. Nie ma tu nic specjalnego. Załóżmy jednak, że mamy nową wersję (v2) i chcemy w bezpieczny sposób ją wdrożyć.  Do dyspozycji mamy zapasowy serwer – w tym przypadku green. Umieszczamy tam zatem nową wersję:

2

Mamy teraz dwie wersje systemu, ale użytkownicy wciąż są przekierowani do v1. Mamy teraz czas na sprawdzenie czy nowy kod nie powoduje żadnych problemów. Oba węzły to serwery produkcyjne, zatem mają dostęp do tych samych zasobów. Jeśli jesteśmy pewni, że wszystko działa, zmieniamy konfigurację balancera, aby przekierowywał ruch do serwera zielonego:

3

Jeśli stwierdzimy, że nie wszystko jednak działa tak jak należy i np. musimy dokonać rollback’u, wtedy sewer niebieski wciąż zawiera poprzednią wersję. Wystarczy ponownie zmienić przekierowanie na balancerze i ruch będzie przekierowywany do V1.

Kolejne wdrożenia polegają na tej samej zasadzie. Wersja v3 będzie zainstalowana na serwerze z poprzednią wersją, czyli serwerze niebieskim w tym przypadku:

4

Innymi słowy, po każdym wdrożeniu, balancer będzie zmieniany tak, aby na drugi węzeł wskazywać. W dowolnym momencie, jeden z serwerów będzie zawierał aktualną wersję, a drugi poprzednią. W przypadku wdrażania nowego kodu, serwer z poprzednią wersją można określić jako staging albo preprod. Jak widać,  nie ma znaczenia, który serwer to “blue”, a który “green”. Powyższy wzorzec ma wiele nazw i myślę, że nazwa może być trochę myląca – “green” nie znaczy, że jest to zawsze aktywny serwer. Aktywny serwera, co każde wdrożenie jest przełączany między niebieskim, a zielonym węzłem.

Podsumowując dzięki blue\green deployment zyskujemy:

  • zero downtime deployment – aplikacja zawsze będzie dostępna dla użytkowników.
  • Rollback – wrócenie do poprzedniej wersji sprowadza się wyłącznie do zmiany konfiguracji routera\balancera.
  • Bezpieczne i bezstresowe wdrożenia.

HTTP 2.0 Server Push

Dzisiaj kolejny element HTTP 2.0, tym razem wymagający zmiany kodu po stronie aplikacji. Tak jak już z wszystkimi opisanymi wcześniej zmianami, ma to na celu zmniejszenie opóźnienia (latency) wynikającego z liczby zapytań.

Doskonale wiemy, że każda strona ma referencje do innych zasobów takich jak CSS czy pliki graficzne. Wcześniej zajęliśmy się już HTTP Multiplexing, który znacząco niweluje problem.

W jednym z poprzednich wpisów pokazałem również jak w HTTP 1.1 programiści radzili sobie z zasobami. Częstym obejściem, było umieszczenie plików graficznych inline. Powodowało to, że jak tylko plik HTML lub CSS został ściągnięty, plik graficzny stawał się od razu dostępny (ponieważ był częścią tego samego pliku).

Z tej techniki tak naprawdę wywodzi się Server Push. Wiemy, że w celu wyświetlenia strony “A”, musimy również wysłać zasób “B”. Dlaczego zatem nie wysłać zasobu B od razu zaraz po A? Inline jest pewną implementacją tego, ale jak wiemy dość kłopotliwą. Server push pozwala z kolei, “wepchnąć” dowolny zasób (nie koniecznie plik graficzny) w strumień danych płynący od serwera do klienta. Oczywiście serwer (np. IIS), nie będzie znał relacji w poszczególnych aplikacjach między zasobami. Wynika z tego, że to nasza rola (ewentualnie framework’a)  jest poinformować o tym klienta.

Służą do tego tzw. “Push Promise”, czyli obietnice klienta odnoście relacji między zasobami. Jeśli projektując system wiemy, że wyświetlając stronę A, zaraz będzie załadowana  biblioteka jQuery,  wtedy wydajemy obietnice. Push promise to prosta metoda, która zwraca listę zależności. Przeglądarka następnie przeczyta taką listę obietnic i stwierdzi czy warto je akceptować. Czasami te zasoby mogą już znajdować się w cache przeglądarki, wtedy należy przerwać taką transmisje. W przeciwnym wypadku przeglądarka zaakceptuje je i w praktyce zostaną one odebrane zaraz po przesłaniu strony – bez zbędnych opóźnień.

W ASP.NET dostępna jest już metoda PushPromise:

public void PushPromise(
	string path,
	string method,
	NameValueCollection headers
)

Drugie przeładowanie jest nieco prostsze w użyciu:

public void PushPromise(
	string path
)

Warto zauważyć, że w taki sposób możemy kontrolować czas życia obiektu. W przypadku inline nie było takiej możliwości – zawsze zasób był przesyłany. Jeśli korzystamy z ServerPush, zasób będący w cache przeglądarki zostanie anulowany i nie przesyłany. Podobnie za pomocą parametru headers (nagłówki), możemy dowolnie aktualizować lub usuwać dany zasób z cache (nagłówek Cache-Control).

Z powyższego opisu wynika jeszcze jeden wniosek – zwracana obietnica zasobu musi nadawać się do cachowania. Przeglądarka ma prawo i będzie wspomniany zasób wykorzystywać w innych podstronach. Tak samo, projektując stronę, nie powinniśmy polegać wyłącznie na Server Push. To jedynie usprawnienie w wydajności, a nie element nowej architektury – wiele przeglądarek wciąż tego nie wspiera.

Server Push to wyłącznie mechanizm uprzedzenia przeglądarki i dostarczania zależności wraz z plikiem głównym. Nie ma to nic wspólnego z Web Sockets i innymi technikami komunikacja dwustronnej.

HTTP 2.0 w IIS (ASP.NET)

IIS w Windows 10 wspiera już od jakiegoś czasu nową wersję protokołu.  W zasadzie prawdopodobnie nic nie musimy robić, jeśli posiadamy prawidłową wersję IIS. Oczywiście użytkownicy muszą posiadać również odpowiednią wersję przeglądarki internetowej. Ich kompatybilność można sprawdzić tutaj. Jak widać, Edge, Chrome, Firefox, Opera czy iOS Safari radzą sobie najlepiej. W przypadku IE, najnowsza wersja wspiera HTTP 2.0 tylko częściowo.

Jeśli jeszcze nie zainstalowaliśmy IIS na Windows 10, wtedy przejdźmy najpierw do “Turn Windows features on or off”, a następnie zaznaczamy Internet Information Service:

1

Aktualnie IIS wspiera wyłącznie HTTPS (TLS). Oznacza to, że jeśli chcemy użyć nieszyfrowanego połączenia to HTTP 1.1 będzie wciąż używany. Dodajmy więc kolejny binding HTTPS:

2

I to wszystko co musimy zrobić… Jak widać,  nic specjalnego nie należy konfigurować. Wystarczy odpowiednia wersja IIS oraz przeglądarki po stronie klienta. Odpalmy zatem stronę zarówno z HTTP, jak i HTTPS. Spodziewamy się, że nieszyfrowane połączenie wciąż będzie HTTP 1.1:

3

W przypadku HTTPS,HTTP 2.0 będzie użyty: 4

Skrót H2 oznacza oczywiście HTTP 2.0

W przyszłym poście opiszę kolejny element HTTP 2.0, w tym przypadku Server Push, który wymaga pewnych modyfikacji w aplikacji ASP.NET. W celu przetestowania tej aplikacji, potrzebny będzie zarówno serwer IIS wspierający HTTP 2.0, jak i odpowiednia przeglądarka (np. Chrome).

HTTP 2 – kompresja danych oraz atak CRIME

HTTP 2.0 jak wiemy z poprzedniego już wpisu jest protokołem binarnym. Wiemy również, że fundamentalne zasady działania HTTP nie zostały zmienione. Oznacza to, że HTTP pozostaje protokołem bezstanowym.  To z kolei w praktyce oznacza, że każde zapytanie musi dostarczać wszelkie informacje potrzebne do odtworzenia stanu aplikacji. Powoduje to, że zarówno ciało zapytania jak i nagłówek po jakimś czasie mogą zawierać dużo informacji, które należy przesyłać w każdym zapytaniu.

Kompresja danych, dostarczona w HTTP 2.0  jest kolejnym usprawnieniem poprawiającym wydajność, a w szczególności opóźnienie. HTTP 2.0 dostarcza również kompresje nagłówków, co kompletnie nie było dostępne w HTTP 1.1 (była możliwość jedynie kompresji ciała).

Kompresja HTTP 2.0 jest również odporna na atak CRIME (Compression Ratio Info-leak Made Easy), na który był podatny nawet klasyczny HTTPS czy poprzednik HTTP 2.0, a mianowicie SPDY. Jak to możliwe, że HTTPS, który jest szyfrowany może cierpieć na niebezpieczny atak ze względu na dodaną kompresję danych?

Jak sama nazwa wskazuje, atak polega na monitorowaniu rozmiaru skompresowanej treści. Prawdopodobnie dodając trochę treści, rozmiar musi się zmienić. Pomimo, że całość jest szyfrowana, to naturalnie rozmiar pakietu jest całkowicie jawny – to informacja, którą atakujący może wykorzystać.

W jaki sposób zatem przedstawiony atak może wyglądać? Załóżmy, że chcemy odgadnąć szyfrowany identyfikator sesji. Gdzieś w ciele zapytania musi być  przechowywana wartość np. “sessionId=252”. Jednym z fragmentów, które możemy kontrolować jest typ i adres zasobu dla zapytań. Jeśli chcemy wysłać GET do strony głównej wtedy zapytanie będzie wyglądać następująco:

GET /home

Wyobraźmy sobie, że wysyłamy jednak  najpierw poniższy pakiet

GET /sessionId=9

Oczywiście cały czas monitorujemy rozmiar pakietu. Następnie kolejny pakiet to:

GET /sessionId=2

Co możemy dowiedzieć się z rozmiarów wysłanych pakietów? Czy będą miały identyczny rozmiar? Jeśli kompresja jest użyta, drugi pakiet (sessionId=2) będzie mniejszy niż pierwszy, ponieważ sessionId=2 występuje już w ciele zapytania, które zawiera sekretny identyfikator sesji. Algorytmy kompresji, w dużym skrócie polegają na zastępowaniu duplikatów pewnymi wskaźnikami – np. skrótami. Naturalne zatem, że po kompresji ciąg  znaków “AABB” będzie miał mniejszy rozmiar niż “ABCD”. Metodą prób i błędów modyfikujemy zatem pakiet (w tym przypadku adres zasobu), aby rozmiar całości redukował się. Jeśli dodanie kolejne cyfry sesji zwiększa rozmiar, to znaczy, że takowa cyfra nie występuje w zaszyfrowanym ciele i stąd kompresja nie przynosi skutków. Jeśli rozmiar jest mniejszy, pewne fragmenty pakietu się powtarzają i oznacza to dla nas, że odgadliśmy kolejną cyfrę.

Widzimy, że przechwycenie zaszyfrowanego pakietu, zawierającego numer sesji może być niebezpieczne dla użytkownika. Pomimo, że nie mamy szans rozszyfrować pakietu, to bazując na rozmiarze możemy spróbować odtworzyć analogiczny pakiet. Podsumowując, do przeprowadzenia CRIME musimy:

  • posiadać próbkę pakietu, zawierającego sekretne dane (np. identyfikator sesji).
  • kompresja danych musi być włączona
  • należy wstrzyknąć pewną treść, np. za pomocą adresu zasobu.

W przypadku HTTP 1.1, polecane jest aby wyłączyć kompresje danych zarówno po stronie klienta (przeglądarka internetowa) jak i serwera. HTTP 2.0 nie jest podatny na ten typ ataku.

Obejścia problemów z HTTP 1.0\HTTP 1.1

Zanim przejdę do kolejnych usprawnień w HTTP 2.0, warto poświęcić chwilę na zastanowienie się jak omijamy problemy HTTP 1.1 dzisiaj. Większość stron wciąż opiera się na HTTP 1.1 i nie najgorzej radzą sobie z wydajnością. Oczywiście sporo jest do poprawy, ale strony działają na urządzeniach mobilnych bez ogromnych opóźnień.

Mimo wszystko, opóźnienie (latency) jest jednym z podstawowych problemów rozwiązanych przez HTTP 2.0.  Zastanówmy się, jak dzisiaj podchodzimy do tego, aby zminimalizować ten problem. Wiemy, że każda strona posiada liczne skrypty JavaScript, arkusze CSS czy pliki graficzne. Każdy zasób jest ładowany pojedynczo. Jeśli zatem mamy jeden plik html, 2 CSS, 5 skryptów JavaScript oraz 10 plików graficznych, razem zostanie wykonanych 18 zapytań do serwera. Jak wiemy z poprzedniego postu, HTTP 1.1 może wspierać co najwyżej HTTP pipelining. Wysyłanie pojedynczych zapytań jest skrajnie wolne na połączeniach satelitarnych czy mobilnych. Nawet jeśli dostawca obiecuje szybki transfer, to wysłanie pojedynczego zapytania i tak będzie wolne. W tym przypadku średni transfer nie ma znaczenia ponieważ zapytania jak i zawartość strony zwykle nie zawierają dużo danych.

Jednym z obejść zbyt dużej liczby plików graficznych jest połączenie ich w jeden wielki plik (spriting). Technika szczególnie popularna w dawnych czasach do tworzenia animacji komputerowych – jeden plik prezentował różne klatki animacji. Następnie w zależności, którą klatkę się chciało wyświetlić, wydzielało się konkretny fragment większego pliku graficznego. Podobną technikę można wykorzystać w web – załadować jeden obrazek (pojedyncze zapytanie), a potem wyświetlać konkretne fragmenty w zależności od potrzeb. Oczywiście rozwiązanie bardzo niewygodne ponieważ należy logicznie scalać obrazki co zwykle jest czasochłonne i trudne.

Kolejna obejście to definiowanie grafiki bezpośrednio w arkuszach CSS. Zamiast odnosić się do zewnętrznych plików graficznych (które wymagają osobnych zapytań), można osadzać dane bezpośrednio w CSS (embedded images).

Programiści ASP.NET z pewnością kojarzą ASP.NET Bundles. Z punktu widzenia jakości kodu, warto rozdzielać kod JavaScript na konkretne moduły. Ma to jednak ogromny wpływ na wydajność – im więcej plików JS tym wolniej załaduje się strona ponieważ należy więcej wysłać zapytań. Rozwiązanie problemu jest proste -scalać wszystkie pliki w jeden wielki skrypt. Wtedy wystarczy, że przeglądarka wyśle jedno zapytanie i wszystko zostanie załadowane.  Kolejną analogiczną techniką jest minifikacja (minification). Polega na usunięciu niepotrzebnych znaków  takich jak np. komentarze, spacje, znaki nowej linii itp. z pliku JavaScript. Nie są one niezbędne do wykonania kodu, a zajmują miejsce.  Dzięki ASP.NET Bundles zostanie to zrealizowane podczas wdrażania aplikacji. Kod zatem pozostanie przejrzyście rozdzielony na różne pliki, a po wdrożeniu, serwer będzie serwował pojedynczy plik.

Kolejną techniką jest jest tzw. sharding. Polega na umieszczaniu tych samych zasobów (np. plików graficznych) na różnych serwerach. Jak wiemy, HTTP 1.1 posiada wyłącznie HTTP pipelining, który w wielu przeglądarkach jest i tak wyłączony ze względu na HOL blocking. Przeglądarki starają się zatem zainicjalizować  wiele połączeń TCP, które stanowią wtedy niezależne kanały komunikacji. Problem w tym, że specyfikacja HTTP mówiła, że klient może nawiązać maksymalnie dwa równoległe połączenia. Dzięki sharding, mamy te same zasoby dostępne z różnych maszyn.  Wtedy okazuje się, że dla każdego serwera możemy nawiązać dwa różne połączenia. Z tego co wyczytałem, dzisiaj można nawiązać więcej równoległych połączeń, ale i tak ze względu na limity korzysta się z sharding.

Niestety wszystkie powyższe techniki mają wady. Oprócz oczywistej, która jest zbyt duża złożoność, wcześniej czy później pojawią się problemy z buforowaniem. Scalanie plików w JS spowoduje, że zmiana w jakimkolwiek pliku wymusi przeładowanie całego skryptu. Podobnie z spriting – modyfikacja jednego obrazka wymaga odświeżenia cache w przeglądarce dla całego sprite’a.