Category Archives: ASP .NET

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.

 

HTTP 2.0 Multiplexing, HTTP 1.1 Pipelining oraz HOL blocking

Kilka postów chcę przeznaczyć na temat HTTP 2.0. Zamiast pisać tylko o nowościach w nowej wersji protokołu, warto najpierw zrozumieć jak działała dotychczasowa wersja czyli HTTP 1.1.

Pierwszym problemem HTTP 1.0 oraz HTTP 1.1 była obsługa wielkiej ilości zapytań. Każda strona, aby prawidłowo wyświetlić się potrzebuje załadować mnóstwo zewnętrznych zasobów takich jak skrypty JS, arkusze css czy grafika. Ponadto HTTP oparty jest na TCP więc ponadto mamy do czynienia z TCP handshake zanim połączenie jest zainicjalizowane.  Połączenia internetowe są coraz lepsze, ale szybkość załadowania stron nie zawsze jest satysfakcjonująca. Wyobraźmy sobie ekstremalną sytuację. Strona posiada 10 skryptów JS, 2 arkusze CS oraz 10 plików graficznych. Zasoby w HTTP nie są przesyłane razem. Najpierw ściągany jest plik HTML, a potem każdy zasób zewnętrzny jest ściągany jeden po drugim. Po mimo szybkiej, średniej szybkości łącza internetowego, opóźnienie (latency) wiążące się z wysoká liczbą zapytań sprawia, że strona działa wolno.

W HTTP 1.0 wszystkie zapytania musiałby być wysyłane pojedynczo (szeregowo). Jeśli mamy zatem 20 zapytań, najpierw musieliśmy wysłać pierwsze, czekać na odpowiedź i dopiero potem wysłać drugie. W przypadku internetu satelitarnego czy mobilnego, strony po prostu wczytywały się bardzo wolno, mimo wysokiej średniej szybkości transmisji.

W odpowiedzi na oczywisty problem, w HTTP 1.1 wprowadzono HTTP pipelining. Dzięki temu, możliwe było wysyłanie kilku zapytań w tym samym połączeniu TCP. Korzyści były ogromne. Po pierwsze tylko jeden TCP Handshake. Zamiast czekać na odpowiedź mogliśmy wysłać najpierw wszystkie zapytania, a potem odbierać odpowiedzi jedno po drugim. Niestety musiało to odbywać się na zasadzie FIFO – odpowiedzi musiały przychodzić w tej samej kolejności co wysłane zapytania. Stanowiło to tak ogromne ograniczenie, że większość przeglądarek nie korzysta dziś nawet z HTTP pipelining.

Podstawowy problem to Head Of Line Blocking. Wiemy, że odpowiedzi muszą przychodzić w tej samej kolejności co zapytania. Co jeśli drugie zapytanie trwa bardzo długo? Skutek będzie taki, że będzie to blokowało kolejne odpowiedzi i przyszłe zapytania. Jeśli zatem wszystkie zapytania byłyby wysyłane za pomocą HTTP Pipeline efekt mógłby być odwrotny do zamierzonego – jeśli pierwszy element zajmuję bardzo długo, wtedy kolejne muszą czekać. Co jeśli ten pierwszy element tak naprawdę nie jest niezbędny do wyświetlenia większości zawartości na stronie? HTTP pipelining zniwelował problemy związane z opóźnieniem, ale ryzyko blokady HOB odstraszało wielu twórców przeglądarek od adaptacji tego.

I tutaj przychodzimy do HTTP 2.0 Multiplexing. Protokół HTTP 2.0 jest binarny, w przeciwieństwie do wersji poprzednich. Wysyłane pakiety stanowią ramki składające się z nagłówku i ciała. Wprowadzono definicje strumieni oraz priorytetów.  Dane wysyłane w obrębie dwóch różnych strumieni są niezależne od siebie, a co za tym idzie, mogą się wzajemnie przeplatać.  Innymi słowy, HTTP 2.0 multiplexing to prawidłowa implementacja wysyłania wielu zapaytań w obrębie tego samego połączenia TCP. Odpowiedzi już nie muszą być wysyłane w tej samej kolejności przez serwer i zostanie to prawidłowo obsłużone. Wspomniana kolejka FIFO nie jest już potrzebna. Ustawienia priorytetów umożliwia uzyskanie odpowiedzi w dowolnej kolejności. Serwer po utrzymaniu wielu zapytań naraz może skorzystać z priorytetów, aby przetwarzać je w odpowiedniej kolejności.

HTTP Multiplexing stanowi jedno z podstawowych usprawnień HTTP 2.0, który wpływa bardzo korzystnie na wydajność stron internetowych, szczególnie w środowisku z wysokim opóźnieniem ( połączenie mobilne). W przyszłych wpisach zajmiemy się kolejnymi usprawnieniami. Dobrą wiadomością jest, że jako programiści nie musimy wiele robić. HTTP 2.0 został zaprojektowany tak, aby mógł zostać wprowadzony bez konieczności aktualizacji istniejącego kodu. Wszystkie pryncypalne zasady takie jak wysyłanie ciasteczek, nagłówki, sesje itp mają nadal rację bytu – HTTP 2.0 to nie zmiana architektury.

HSTS w ASP.NET z użyciem biblioteki NWebsec

W poprzednim poście opisałem zasadę działania protokołu HTTP Strict Transport Security. W skrócie najważniejsze punkty to:

  • Serwer zwraca specjalny nagłówek “Strict-Transport-Security”, który powinien być przesyłany wyłącznie przez HTTPS.
  • Po otrzymaniu nagłówka od serwera, przeglądarka zawsze będzie łączyć się przez HTTPS, a nie HTTP. Użytkownik jeśli nawet będzie chciał użyć HTTP, przeglądarka dokona wewnętrznego przekierowania na HTTPS (307 – internal redirect).

Oczywiście można samemu zwracać odpowiedni nagłówek, ale lepiej skorzystać z gotowych pakietów nuget. Często programiści zawsze zwracają strict-transport-security, co jest również błędne – specyfikacja mówi wyraźnie, że powinno go zwracać się wyłącznie przez https.

Skorzystamy zatem z pakietu NWebsec.  Zestaw zawiera wiele różnych bibliotek związanych z bezpieczeństwem aplikacji webowych. W naszym przypadku zainteresowani jesteśmy wyłącznie HSTS.

Po zainstalowaniu,  wystarczy w OWIN startup:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseHsts(options => options.MaxAge(seconds: 10886400).IncludeSubdomains().Preload());
        }
    }

Za pomocą FluentInterface możemy łatwo zmodyfikować każdy parametr nagłówka. Jeśli odpalimy teraz aplikacje, zobaczymy, że faktycznie nagłówek jest zwracany:

1

Warto jednak wciąż wymuszać globalnie HTTPS za pomocą atrybutu RequiresHttpsAttribute:

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new RequireHttpsAttribute());
        }
    }

Ważne to jest dla pierwszego odczytania HSTS oraz stanowi to drugą warstwę bezpieczeństwa (np. gdyby dana przeglądarka nie wspierała HSTS).

Czasochłonne operacje w ASP.NET – Hangfire

W jednym z poprzednich wpisów, pokazałem jak prawidłowo tworzyć wątki w tle w celu wykonania jakieś czasochłonnej operacji w ASP.NET. Pod wpisem, Michal Dymel zaproponował narzędzie o nazwie Hangfire.
Muszę przyznać, że framework jest bardzo prosty w użyciu i nie ma zbyt wiele zewnętrznych zależności. Od kilku tygodni korzystam z niego w jednym ze swoich projektów i nie miałem żadnych problemów z konfiguracją czy wdrążaniem rozwiązania w system produkcyjny.

Po kolei jednak… Hangfire służy do wykonywania czasochłonnych operacji w ASP.NET. Dostarcza zatem model “fire&forget”, czyli bardzo prostą implementację systemu kolejkowego. Jeśli zaplanowane zadanie niepowiedzie się (np. timeout), wtedy zostanie automatycznie kilkakrotnie razy powtarzane, w różnych odstępach czasu (tak jak w nServiceBus).
Wiemy, że IIS może zamknąć proces w dowolnym momencie, kończąc tym samym wszystkie zadania, które były wykonywane w tle. Dzięki Hangfire, nie musimy się martwić o wznawianie takiej operacji.

Zaplanowane zadania, przechowywane są w bazie danych, np. SQL Server czy Redis. Istnieje możliwość zaimplementowania własnego repozytorium, ale jest to dość skomplikowane.

Ponadto, Hangfire dostarcza bardzo przyjazny interfejs użytkownika. Instalacja jest bardzo prosta – wystarczy zainstalować poniższy pakiet NuGet:

 Install-Package HangFire

Następnie w Owin startup, umieszczamy:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration
              .UseSqlServerStorage(@"Server=PIOTR\SQLEXPRESS;Database=HangfireDb;Trusted_Connection=True;");

            app.UseHangfireDashboard();
            app.UseHangfireServer();
        }
    }

Powyższy przykład bazuje na SQL Server jako repozytorium zadań. Hangfire sam stworzy wymaganą strukturę tabel. Po uruchomieniu aplikacja, baza danych zostanie zainicjalizowana następującymi tabelami:

hangfiredb

W celu zaplanowania zadania, które wykonuje się o określonym czasie wystarczy:

RecurringJob.AddOrUpdate("Nazwa zadania",() => Console.WriteLine("Zaplanowane zadanie...."),Cron.Minutely);

Zwykle powyższy kod umieszcza się w Startup. Możemy przekazać jako parametr dowolne wyrażenie CRON. Hangfire zadba, aby zadanie wykonywało się o określonych porach oraz zostało wznawiane w przypadku recycling dokonywanego przez IIS.

Możemy również dodać zadanie do kolejki,  jeśli chcemy, aby zostało wykonane tylko raz:

BackgroundJob.Enqueue(
    () => Console.WriteLine("Test"));

Dużym udogodnieniem jest dostarczenie UI w formie dashboard. Przechodząc na stronę /hangfire (np. http://localhost:63486/hangfire), zobaczymy diagram pokazujący liczbę wykonanych zadań w ciągu ostatniego tygodnia lub dnia:

dashboard

W zakładce “Recurring tasks” zobaczymy nasze zaplanowane zadania:

recurring_tasks

Mamy szybki i łatwy podgląd, kiedy ostatnio zadanie zostało wykonywane i z jaką częstotliwością będzie wykonywane.

Przechodząc do zakładki Jobs, zobaczymy listę dostępnych kolejek:

queues

Widzimy, że aktualnie mamy 9 zadań w “Succeeded”.  Hangfire zadba o tym, aby nie zapychać powyższych kolejek i każdego dnia niepotrzebne wpisy zostają usuwane. Przechodząc do jakiejkolwiek kolejki, możemy przyjrzeć się szczegółom wykonanemu zadaniowi:

task_sucess

Załóżmy, że nasze zadanie teraz będzie wyrzucało wyjątek:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration
                .UseSqlServerStorage(@"Server=PIOTR\SQLEXPRESS;Database=HangfireDb;Trusted_Connection=True;");

            app.UseHangfireDashboard();
            app.UseHangfireServer();

            RecurringJob.AddOrUpdate("Nazwa zadania", () => SampleTask(), Cron.Minutely);
        }

        private static void SampleTask()
        {
            throw new NotImplementedException();
        }
    }

W zakładce Retries zobaczymy wtedy wpis informujący nas, że zadanie nie zostało wykonywane i Hangfire próbuje je wykonywać powtórnie:
retries

Oczywiście po pewnym czasie, gdy próby nie przyniosą efektu, zadanie zostanie na stałe przeniesione do “Failed” (gdzie ręcznie możemy wznowić wykonywanie).

Przechodząc do szczegółów zadania, zobaczymy informacje o wyjątku:

error_details

Ponadto HangFire integruje się z popularnymi loggerami (np. nLog, log4net) i jakiekolwiek logi wykonane przez Hangfire będą widoczne w naszych logach. Nic nie trzeba konfigurować – Hangfire automatycznie wykryje, aktualnie wykorzystywanego w systemie loggera.

ASP.NET – wykonywanie czasochłonnej operacji w wątku

O asynchronicznych kontrolerach pisałem już na blogu, zarówno w czystym ASP.NET jak i ASP.NET MVC.

Czasami jednak chcemy zaimplementować model na wzór “fire&forget”. Oczywiście do tego, dużo bardziej nadają się systemy kolejkowe typu nServiceBus, ale dla bardzo prostych przypadków wystarczy odpalenie wątku i wykonanie jakieś czasochłonnej operacji. Przez czasochłonną mam na myśli taką, która wykonuje się kilka minut, a nie kilka godzin.  Jeśli mamy aż tak skomplikowane operacje, wtedy wykonywanie tego w czystym WebAPI\Web zdecydowanie jest złą praktyką.

Błędnym podejściem wykonania operacji w tle jest:

 public ActionResult ExecuteSomething()
 {
    Task.Factory.StartNew(() =>
    {
	Thread.Sleep(5000);
        // time-consuming operation
    });

    return View("Index");
}

“Zwykły” wątek niestety może być zakończony w każdej chwili przez IIS. W momencie zakończenia akcji, IIS przyjmuje, że pozostałe wątki nie mają znaczenia. W praktyce oznacza to, że restart aplikacji albo jej puli może nastąpić w każdym momencie.

Od .NET Framework 4.5.2 istnieje lepszy sposób, a mianowicie HostingEnvironment.QueueBackgroundWorkItem:

 HostingEnvironment.QueueBackgroundWorkItem((token) =>
 {
      Thread.Sleep(5000);
     // time-consuming operation
 });

Jak widać, kod nie wiele różni się, ale w tym przypadku IIS będzie świadomy zadania. Metoda zawiera również token, który używać należy w celu anulowania zadania (np. gdy aplikacja jest zamykana w IIS, a zadanie jest wciąż wykonywane w tle).

Jak wspomniałem nie jest to przeznaczone dla bardzo długich operacji. Jeśli zależy nam na gwarancji wykonania, wtedy zdecydowanie odradzam używanie tego. Autorzy metody nie dają takiej pewności i jeśli aplikacja webowa będzie musiała zostać zakończona, QueueBackgroundWorkItem może jedynie opóźnić zakończenie AppDomain (o maksymalnie 90 sekund).
HttpContext również domyślnie nie zostanie przekazany.

Cross-Origin Request sharing (CORS): Atrybut EnableCors

O atrybucie EnableCors już wspomniałem w pierwszym poście o CORS.  Dzisiaj chciałbym przyjrzeć mu się dokładniej.  Dla przypomnienia, najczęściej dekoruje im się kontrolery:

    [EnableCors( "http://localhost:24018","*","*")]
    public class ValuesController : ApiController
    {
        // GET api/values/5
        public string Get()
        {
            return "Hello World@";
        }
    }

Możliwe jest jednak dołączenie go do pojedynczej metody (akcji):

    public class ValuesController : ApiController
    {
        [EnableCors("http://localhost:24018", "*", "*")]
        public string Get()
        {
            return "Hello World";
        }
    }

Jeśli chcemy nałożyć atrybut globalnie, wtedy w WebConfig możemy:

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {

            var cors = new EnableCorsAttribute("http://localhost:24018", "*", "*");
            config.EnableCors(cors);

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }

Origin określa adres klienta czyli konsumenta danej usługi. W powyższym przypadku wyłącznie http://localhost:24018 uzyskuje dostęp do API. Możemy przekazać gwiazdkę (*) jeśli chcemy zezwolić wszystkim klientom:

[EnableCors(origins: "*", 
    headers: "*", methods: "*")]

Oczywiście odradzam akceptowanie wszystkich klientów ze względu na bezpieczeństwo. Każde zapytanie AJAX, będzie mogło korzystać z naszego API, co potencjalnie może być wykorzystane np. w phishing.
Dozwolone jest również przekazanie listy adresów po przecinku:

[EnableCors(origins: "http://www.address1.com,http://www.address2.com"", 
    headers: "*", methods: "*")]

We wszystkich parametrach można podać listę wartości po przecinku. Na przykład:

[EnableCors(origins: "http://www.address1.com", 
    headers: "accept,content-type,origin,x-my-header", methods: "*")]

Niestety różne przeglądarki interpretują inaczej headers i nie można ma tym polegać. Cały mechanizm CORS mocno polega na implementacji przeglądarki. To ona w końcu decyduje jakie pakiety wysłać do serwera i kiedy zezwolić na połączenie. Zdecydowana większość przeglądarek nie pozwala na między domenowe połączenia, ale nie wszystkie w pełni implementują CORS. Może zdarzyć się, że po prostu lista nagłówków nie zostanie wysłana jako część zapytania CORS.

Analogicznie sprawa wygląda z metodami HTTP:

[EnableCors(origins: "http://www.address1.com", 
    headers: "*", methods: "GET,POST")]

To ostatni post o CORS. W następnym wpisie powracam do zaczętej w poprzednim tygodniu tematyki o bezpieczeństwie aplikacji WEB.

Cross-Origin Request sharing (CORS): Zapytania prefight

Po ostatnim poście powinno być jasne dlaczego i kiedy warto używać CORS. Przedstawiony przykład pokazywał dwa kluczowe nagłówki: origin oraz Access-Control-Allow-Origin. W praktyce jednak, może zdarzyć się, że przeglądarka wyśle dodatkowy pakiet, tzw. “prefight”. Przeglądarki omijają ten etap, gdy następujące warunki sa spełnione:

  1. Zapytanie jest typu GET, HEAD lub POST
  2. W nagłówku nie ma innych zapytań niż  Accept, Accept-Language, Content-Language lub Content-Type
  3. Content-Type ma wyłącznie wartości takie jak: application/x-www-form-urlencoded, multipart/form-data, text/plain

W przeciwnym wypadku, pakiet prefight zostanie wysłany. Najprostszy przykład takiego pakietu, wysyłanego przez klienta do usługi to:

OPTIONS /
Host: bar.com
Origin: http://foo.com

Przede wszystkim, prefight używa HTTP Options, gdzie jako wartość często podaje się adres usługi. Dodatkowo, można skorzystać z nagłówków Access-Control-Request-Method lub Access-Control-Request-Headers. Pierwszy z nich służy do określenia metod HTTP z jakich chcemy skorzystać (GET, PUT itp.). Drugi z kolei, zawiera dodatkowe nagłówki, jakie klient chce ustawić (niestandardowe). Przykład:

OPTIONS http://service.com/hello HTTP/1.1
Accept: */*
Origin: http://client.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: accept, x-my-custom-header
Accept-Encoding: gzip, deflate

W odpowiedzi z kolei, dostajemy standardowo Access-Control-Allow-Origin oraz dodatkowo Access-Control-Allow-Methods:

Access-Control-Allow-Origin: http://foo.com
Access-Control-Allow-Methods: PUT, DELETE

W przypadku, gdy wszystko zgadza się (origin, methods) wtedy dopiero przeglądarka wysyła właściwe zapytanie między domenowe. Jak widać, w przypadku niestandardowych zapytań, czas wykonania może być trochę dłuższy ze względu na liczbę wysyłanych i odbieranych pakietów.

Cross-Origin Request sharing (CORS): wywoływanie zewnętrznych usług z JavaScript

Kilka postów wcześniej pisałem o JSONP, jako sposobie na wywoływanie serwisów znajdujących się w innych domenach z poziomu JavaScript. Domyślnie przeglądarki blokują takie wywołania ze względu na bezpieczeństwo. Załóżmy, że mamy następujący serwis w jakiejś domenie:

public class ValuesController : ApiController
{
   // GET api/values/5
   public string Get()
   {
      return "Hello World";
   }
}

Następnie w drugiej domenie mamy kod JavaScript próbujący pobrać dane z powyższej usługi:

$.ajax({ url: "http://localhost:24523/api/Values" }).
done(function (data) { $("#testLabel").text(data.Text); });

Próba połączenia się z usługą zakończy się oczywiście następującym błędem:

“XMLHttpRequest cannot load http://localhost:24523/api/Values.
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:24018’ is therefore not allowed access.”

CORS to standard zaimplementowany przez większość współczesnych przeglądarek internetowych.  W skrócie jeśli kod JavaScript chce wykonać zapytanie cross-domain to przeglądarka najpierw wyśle specjalny pakiet do usługi. Jeśli usługa wyrazi zgodę na cross-domain, wtedy połączenie między domenowe zostanie nawiązane. Innymi słowy, to serwer decyduje czy dopuścić dane połączenie z obcej domeny. Z punktu technicznego zatem, zarówno przeglądarka jak i serwer muszą wspierać CORS. W WebAPI bardzo prostą możemy zaimplementować CORS. Wystarczy, że zainstalujemy następujący pakiet:

Install-Package Microsoft.AspNet.WebApi.Cors

Następnie w WebConfig wywołujemy EnableCors:


        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            config.EnableCors();

            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

 

Należy również oznaczyć dany kontroler atrybutem EnableCors:

    [EnableCors( "http://localhost:24018","*","*")]
    public class ValuesController : ApiController
    {
        // GET api/values/5
        public string Get()
        {
            return "Hello World@";
        }
    }

 

Po uruchomieniu strony, wszystko załaduje się prawidłowo. Jak widać jest to trochę prostsze niż JSONP i bardziej naturalne. JSONP to tak naprawdę wykorzystywanie pewnej luki w przeglądarkach, aczkolwiek jest to bardzo powszechna praktyka i nic nie stoi na przeszkodzie, aby po prostu używać JSONP.

Zajrzyjmy jeszcze do definicji atrybutu EnableCors:


public EnableCorsAttribute(string origins, string headers, string methods)
: this(origins, headers, methods, (string) null)
{
}

Jak widać, najważniejszy parametr to origin czyli adres strony, która będzie wywoływała daną usługę. Innymi słowy, w powyższym rozwiązaniu akceptujemy wyłącznie domenę http://localhost:24523. Możemy również być bardziej wybredni co do przychodzących zapytań i określić konkretne nagłówki czy metody HTTP (GET\POST itp.).

Przyjrzyjmy się również pakietom jakie przeglądarka i usługa wysyłają. W momencie, gdy klient (przeglądarka) próbuje nawiązać połączenie między domenowe, przeglądarka wyśle pakiet z nagłówkiem Origin równym adresowi klienta czyli w tym przypadku “Origin: http://localhost:24018”:


Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:en-GB,en;q=0.8,en-US;q=0.6,pl;q=0.4
Cache-Control:max-age=0
Connection:keep-alive
Host:localhost:24523
Origin:http://localhost:24018
Referer:http://localhost:24018/Home/Index
User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36

Jeśli domena pokryje się z regułami opisanymi za pomocą EnableCors, wtedy zostanie zwrócony pakiet z nagłówkiem Access-Control-Allow-Origin równym danej domenie tzn.:


Access-Control-Allow-Origin:http://localhost:24018
Cache-Control:no-cache
Content-Length:14
Content-Type:application/json; charset=utf-8
Date:Mon, 03 Aug 2015 18:09:57 GMT
Expires:-1
Pragma:no-cache
Server:Microsoft-IIS/10.0
X-AspNet-Version:4.0.30319
X-Powered-By:ASP.NET
X-SourceFiles:=?UTF-8?B?YzpcdXNlcnNccGlvdHJ6XGRvY3VtZW50c1x2aXN1YWwgc3R1ZGlvIDIwMTVcUHJvamVjdHNcV2ViQXBwbGljYXRpb24yXFdlYkFwcGxpY2F0aW9uMlxhcGlcVmFsdWVz?=

 

W przyszłym poście opiszę jeszcze kilka rzeczy związanych z CORS. Na zakończenie zachęcam na zapoznanie się, które przeglądarki posiadają wsparcie dla CORS: http://caniuse.com/#feat=cors

 

ASP.NET MVC 6 – AppSettings

W ostatnim wpisie wspomniałem, że Web.Config został usunięty i zastąpiony project.json. Pokazałem, jak dodać referencje czy skonfigurować wersję framework’a. Web.Config jednak zawierał jeszcze jedną ważną sekcję – AppSettings.

ASP.NET 5 wspiera różne typy plików konfiguracyjnych – JSON, INI oraz XML. Możemy wszystkie je dodać jednocześnie do solucji i nie spowoduje to konfliktów. Nie jesteśmy ograniczeni tylko do jednego typu czy pojedynczego pliku konfiguracyjnego. Dla testów, dodajmy 3 pliki, każdy w innym formacie:

JSON.json:

{ "jsontimeout": "1000" }

INI.ini:

initimeout=100

XML.xml:

<root> <xmltimeout>1000</xmltimeout> </root>

Następnie w Startup.cs musimy wskazać powyższe pliki za pomocą:

configuration.AddJsonFile("JSON.json"); configuration.AddIniFile("INI.ini"); configuration.AddXmlFile("XML.xml");

Zmienna configuration jest typu  IConfiguration.  Teraz w dowolnym miejscu, za pomocą również IConfiguration możemy:

string xmlValue = Configuration["xmltimeout"]; string initimeout = Configuration["initimeout"]; string jsontimeout = Configuration["jsontimeout"];

Jak widzimy, wersja ASP.NET 5 daje nam większe możliwości jeśli chodzi o przechowywanie wartości konfiguracyjnych. Nie jesteśmy ograniczeni wyłącznie do Web.config i formatu XML.

Wartości mogą być duplikowane, ale zawsze klucz zarejestrowany później nadpisuje wszystkie starsze.

Ponadto, możliwe jest przechowywanie całych struktur, a nie tylko typów prostych. Załóżmy, że w xml.xml mamy:

<Person> <FirstName>Piotr</FirstName> <LastName>Zielinski</LastName> </Person>

W C# z kolei, tworzymy reprezentację za pomocą klasy:

public class Person { public string FirstName { get; set; } public string LastName { get; set; } }

Wartość możemy przeczytać za pomocą metody Configuration.Get lub wstrzyknąć ją:

services.Configure<Person>(Configuration);

Po wstrzyknięciu, w każdym kontrolerze będziemy mieli dostęp do powyższych danych:

public class HomeController : Controller { private readonly IOptions<Person> _person; public HomeController(IOptions<Person> person) { _person = person; } }

Myślę, że to dobra zmiana, aczkolwiek konwersja aplikacji ze starszych wersji do MVC 6 zdecydowanie jest trudna i czasochłonna.