Tomasz Kowalczyk Blog::Programisty

18cze/1023

[JavaScript] Wysyłanie żądania AJAX w odrębnej domenie.

Jakiś czas temu wpadłem na pomysł napisania skryptu pozwalającego na sprawdzanie pozycji danej frazy w wyszukiwarkach na własną rękę. Już na samym początku natrafiłem jednak na istotny problem - w jaki sposób pobrać "stronę" wyszukiwania wysyłając do niej tradycyjne żądanie AJAX? W dzisiejszym wpisie pomogę Wam rozwiązać ten problem.

Wstęp.

Każdy, kto kiedykolwiek "bawił się" AJAXem zapewne kojarzy pojęcie "same origin policy" [wolne tłumaczenie: "polityka tego samego źródła"]. W wielkim skrócie powiem, że dotyczy ona możliwości [a raczej jej braku] wysyłania żądań do różnych serwerów oznaczonych inną niż nasza domeną. Jako, że moja definicja może być nieco niejasna, pozwolę sobie posłużyć się przykładem.

Nasz przykładowy skrypt został załadowany przez użytkownika na serwerze example.com. W ramach domeny możemy wysyłać dowolne żądania AJAXowe - tzn. wszelkie próby dostępu do URLi:

example.com?type=ajax&action=getcomments
example.com?type=ajax&action=checkstatus

są przesyłane w mgnieniu oka do użytkownika. Problem powstaje w momencie, kiedy potrzebne stają się dane, które można uzyskać jedynie odwołując się do domeny sameorigin.net [ogólnie: domeny innej niż ta, na której działa skrypt, w tym wypadku jest to example.com]. Wykonujemy żądanie do serwera:

sameorigin.net?type=ajax&action=getexternaldata

i... nic. Myślimy sobie - "pewnie gdzieś mam błąd w kodzie" - i niestety w ten sposób popełniamy błąd, bo takiego żądania po prostu w ten sposób wykonać się nie da. Można jednak w łatwy sposób to zabezpieczenie obejść - opis poniżej.

Rozwiązanie.

Aby obejść zabezpieczenie same origin policy użyjemy tzw. "server side proxy" [wolne tłumaczenie: proxy po stronie serwera]. Nie jest ono obecne tam, gdzie mamy "fizyczny" dostęp do wszelkich narzędzi systemowych, a więc nie dotyczy też języków wykonywanych po stronie serwera. Najpierw zobaczmy w jaki sposób można wykonać żądanie z poziomu języka JavaScript [posługuję się oczywiście genialną biblioteką jQuery ;]]:

$.ajax({
	url: proxyLink,
	dataType: 'text',
	async: false,
	success: function(data, status, xhr)
		{
		contents = data;
		success = true;
		return;
		},
	error: function(xhr, text, code)
		{
		alert('readyState: ' + xhr.readyState
			+ '\nstatus: ' + xhr.status
			+ '\nresponseText: ' + xhr.responseText);
		return;
		}
	});

Jest to fragment nieco wyrwany z kontekstu [jak już dopracuję ten skrypt będziecie się mogli z nim zapoznać i odkryć kontekst ;]], ale widzimy na nim, że ustawiona wcześniej w skrypcie zmienna proxyLink przechowuje adres, z jakiego chcemy pobrać dane, a dwie funkcje: success() i error() pozwalają nam odpowiednio zareagować na rezultat żądania. Teraz zapewne zastanawiacie się w jaki sposób użyjemy naszego proxy. Otóż, skorzystamy z zewnętrznego skryptu PHP, który będziemy musieli umieścić na naszym serwerze:

<?php
header("Content-Type: text/html; charset=utf-8");
$url = $_GET['query'];
$handle = fopen($url, 'r');
if($handle)
	{
    while(!feof($handle))
		{
        $buffer = fgets($handle, 4096);
        echo $buffer;
    	}
    fclose($handle);
	}
?>

Jest to jeden z najprostszych sposobów na jakie można wykonać ten skrypt. Oczywiście można też skorzystać z biblioteki cURL lub podobnej, jednak ja potrzebowałem tego kodu na szybko, dlatego uprościłem sobie sprawę.

Mając taki skrypt na naszym serwerze możemy odwołać się żądaniem AJAXowym do niego, podając adres zewnętrznego serwera, z którego chcemy pobrać dane, tj.:

example.com?type=ajax&link=sameorigin.net%3faction%3dgetexternaldata

i gotowe. Skrypt nie zaprotestuje ani przez chwilę, ponieważ znajduje się w naszej domenie, także wszelkie narzędzia broniące zabezpieczenia same origin policy "nie mają nic do gadania".

Podsumowanie.

W ramach komentarza do artykułu powiem, że nie należy się denerwować takimi "utrudnieniami" - zabezpieczenie same origin zostało wymyślone m. in. po to, żeby niemożliwe były zmasowane ataki CSRF - wyobraźmy sobie, że wchodząc na stronę internetową ta wykonuje żądanie usunięcia naszego konta w innym serwisie. Nie brzmi przyjemnie, prawda?

Chętnie usłyszę w jaki sposób Wy poradziliście sobie z tego typu problemami. Czlowiek uczy się całe życie, a ja, nawet pomimo danych pobieranych ze wszystkich kanałów RSS w jakie wgryzam się każdego dnia nie jestem w stanie posiąść całej istniejącej wiedzy. Z góry dzięki za wszelkie komentarze!

Warto przeczytać.

Trwa ładowanie...

Komentarze (23) Trackbacki (1)
  1. W przypadku AJAXa nie ma innej możliwości. Alternatywą jest Flash lub Silverlight.
    Zastawia mnie natomiast dlaczego dane przesyłasz tekstem, a nie jako XML/JSON oraz dlaczego łączysz się synchronicznie?

  2. Tak jak napisałem, to jest kod stworzony jakiś czas temu, więc nie pamiętam już czemu podjąłem akurat takie decyzje. Aczkolwiek z tego co pamiętam przesyłane dane do skryptu nie były „poprawnym” kodem [X]HTMLa i interpreter robił z nimi „złe rzeczy” podczas zwracania wyniku. Jeśli chodzi o łączenie synchroniczne, to nie wiem dlaczego, ale nie byłem w stanie w żaden sposób podpiąć żądania asynchronicznego tak, żeby działało i umieszczało dane w dokładnie tym miejscu, które chciałem, stąd musiałem się „wyręczyć” takim sposobem – swoją drogą na to już znalazłem rozwiązanie, ale muszę znaleźć czas, żeby to przerobić w inny sposób, bo na razie jeszcze trzyma mnie sesja.

  3. W ogóle, wiele ludzi twierdzi, że Same Origin Policy jest ograniczeniem samego JS`a (Ajax`a), i na pytanie – „czy da się… ” odpowiadają – „Nie, JS tego nie umie” :D
    Spotkałem się z nazwą „budowanie bridge`a” na technikę, którą przedstawiłeś, jak dla mnie nazwa pasuje idealnie i sam niejednokrotnie jej użyłem. Inaczej można to zrobić oczywiście techniką ukrytej ramki, ale to raczej jak nie mamy PHP na serwerze (czy tam czegoś innego server-site) bo to więcej roboty po prostu. Ogólnie miałem pisać na swoim blogu o tym, pewno i napiszę i rozwinę trochę tę myśl ;)

  4. Pominę milczeniem nazwę „budowanie bridge’a”, bo sam staram się tak nie kaleczyć naszego języka, w każdym razie na pewno rozwiązanie które przedstawiłem jest chyba najczęściej używanym. Co do ukrytej ramki – musielibyśmy wtedy w niej także ładować kod JS obsługujący konkretną zmianę, stąd jest to nieco mniej optymalne niż wspomniany „most”. Jak już napiszesz o tym u siebie, to będę wdzięczny za linka w komentarzu – zawsze trochę więcej informacji. ;]

  5. Nazewnictwo jak nazewnictwo, ale ja za art. bardzo dziękuję. Na co dzień siedzę w Ruby On Rails i takimi rzeczami się nie przejmuję (może jednak jest prawdą że Frameworki ogłupiają) ale teraz robię drobną, lecz skomplikowaną modyfikację Joomla i tak prostego i jasnego rozwiązania na odzyskanie danych z xml synchronizowanej aplikacji potrzebowałem. Bardzo dziękuję!!!

  6. Nie ma sprawy – zapraszam na kolejne wpisy już po wakacjach. ;] Jeśli miałbyś jakieś inne problemy – pisz na maila albo zostaw komentarz do wpisu o podobnej tematyce.

  7. Jest jeszcze jedna metoda umożliwiająca obejście tego zabezpieczenia, bez wywołań AJAX-a.

    Najprostsze (kieruje żądanie jednostronne):

    var image = new Image();
    image.src = ‘http://someserver.com/myscript';

    Żądanie zwracające wartości (z wykorzystaniem JSON):

    var script = document.createElement(‘script’);
    script.type = ‘text/javascript’;
    script.src = ‘http://someserver.com/myscript?callback=myFunction';
    document.getElementByTagName(‘body’)[0].appendChild(script);

    function myFunction(data) {
    var data = eval(data);
    // whatever
    }

    Btw. zamiast eval na zwróconych danych można dać bezpieczniejsze rozwiązanie: http://json.org/js.html

    Rozwiązanie to oczywiście wymaga trochę więcej wysiłku, jednak warto znać też tą metodę :-)

  8. Także czytałem o tym wcześniej, dziękuję za merytoryczne rozszerzenie treści wpisu. Twój sposób jest o tyle lepszy, że nie wymaga tworzenia specjalnego proxy, a operuje na samym „API” aplikacji po stronie serwera.

  9. @Kilas: a można prosić o jakiś szerszy opis dla laika?

  10. Proponuję podpiąć się pod oryginalny komentarz, wtedy będzie bardziej widoczny. Generalnie nie widzę, żeby autor miał powiadomienia do komentarzy w tym wpisie, więc najlepiej jakbyś go zapytał samodzielnie na jego blogu – chyba, że rozwiniesz nieco swoją wypowiedź o konkretne pytania, wtedy ja chętnie odpowiem. ;]

  11. Znalazłem bardzo fajny plugin http://james.padolsey.com/javascript/cross-domain-requests-with-jquery/ ale teraz mam problem z pobraniem wartości z xmla, jakieś sugestie ?

  12. Z tego co widzę, w callbacku „success” funkcja przyjmuje jeden argument, który jest treścią zwracaną przez wywołanie AJAXowe. Musisz tą wartość przypisać gdzieś „na zewnątrz” i przetwarzać dalej bez problemu.

    Generalnie, jeśli masz problem programistyczny, to bardzo miło byłoby, gdybyś mógł zamieścić chociaż fragment kodu, który próbujesz wykonać. Pomoc „w ciemno” rzadko dochodzi do szczęśliwego końca. ;]

  13. No dobrze, ten kod brzmi sensownie, ale w którym miejscu skrypt przestaje się zachowywać tak, jakbyś chciał? Ja na podstawie analizy kodu mogę zauważyć kilka problematycznych miejsc:

    * success: function(res) – czy sprawdziłeś, że dane są pobieranie i zapisywane w zmiennej res? Jeśli res jest pusty, to znaczy, że wystąpił błąd pobierania danych, biblioteka ma błędy i nie radzi sobie z żądaniami cross-domain, masz błędny adres pobierania [nie sprawdzałem] lub też Twoja przeglądarka nie do końca chce Ci przekazać rzeczonego XMLa.
    * var headline i dalej – jeśli masz XMLa w res, to czy na pewno możesz w ten sposób wyszukać element w kodzie? Nie jestem pewien, czy na podstawie takiego drzewa XML JavaScript będzie w stanie zbudować drzewo DOM, jeśli moje obawy się potwierdzą, to musisz użyć wyrażenia regularnego w stylu /<email>(*)+</email>/ do wyciągnięcia emaila z tego tekstu.
    * $(„#ajaxcont”).html(”+headline); – ponadto co do poprzedniego punktu standardowo musisz sprawdzić, czy element na którym operujesz istnieje i najpierw „wyalertować” to, co chcesz podstawić zamiast od razu iść „na gotowe”.

    Wydaje mi się, że to wszystkie miejsca w których mogłeś się potencjalnie pomylić, jeśli to nie pomoże, to postaram się w wolnym czasie sam zajrzeć do tego kodu i przetestować jego działanie.

  14. Paweł, dlaczego chcesz ten problem rozwiązać po stronie klienta, a nie serwera (z cache)? Toć to prosta usługa sieciowa, zapewne XML-RPC lub REST.

  15. res nie sprawdzałem, ale skrypt wchodzi do success, error też mam, więc chyba jest OK
    sprawdzę wyrażenia regularne, może tak zadziała

  16. To, że skrypt „wchodzi do success()” nic nie znaczy – zawsze może być do niego przekazany pusty wynik. Sprawdź wszystkie punkty, które opisałem, może trafisz na coś ciekawego. ;]

  17. @Kilas nie wiem dlaczego po prostu zdenerwowałem się na usługę paczkomaty24/7 Mam platformę sklepową shopify i integracja jaka u mnie wchodzi w grę to poprzez XMLowe GET i POSTY. Nie wiedziałem jak się za to zabrać dlatego pomyślałem o JS, ale chyba mogę mieć problem z postami:) Shopify jest napisane w RoR i skryptów PHP nie wykonuje.
    Masz jakiś pomysł?

  18. Użyć odpowiednich narzędzi w RoR? W jaki sposób nie wykonuje skryptów PHP? Przecież jeśli wykonujesz żądanie zdalnie to nie ma znaczenia co za nim stoi, może to być nawet skrypt basha, byle zwracał odpowiednie dane.

  19. Chodzi mi właśnie o wywołanie zdalnej funkcji i obrobienie tego co ona zwróci.

  20. Paweł,

    Napisałem co nieco w tym temacie:
    http://blog.kamilbrenk.pl/cross-domain-javascript/

    Mam nadzieję, że notka w czymś pomoże.
    Pozdrawiam

  21. Mam wrażenie, że jednak jest to możliwe, nawet na kilka sposobów:
    http://www.yarpo.pl/2011/05/06/odczytywanie-danych-ze-zdalnego-serwera/

    Wliczając czysty obiekt XHR

  22. Oczywiście, że to jest możliwe – stąd też niniejszy wpis. Reszta możliwości wymaga jednak moim zdaniem o wiele więcej kombinowania – co nie znaczy, że nie jest to możliwe. Dzięki za uzupełnienie wpisu. ;]
    Ostatni wpis: Konferencja Falsy Values 2011


Leave a comment

(required)

CommentLuv badge

Subscribe without commenting