PostMan i przeglądarka

PostMan i przeglądarka

Wstęp

Ostatnio miałem taką sytuację, że potrzebowałem zalogować się na pewien serwis z użyciem przeglądarki i potem na nim pracować PostMan’em. Było to niebywale upierdliwe, dlatego też zadałem pytanie: "Hej, czy nie można by uruchomić przeglądarki z PostMana?" Okazuje się, że można. I można duuuużo więcej.

Chromedriver

Przede wszystkim musisz zainstalować Chromedriver. To taki serwer działający lokalnie na określonym porcie (domyślnie 9515). Gdy otrzyma odpowiedni request, uruchamia przeglądarkę (Chrome), ale może też robić z nią różne dziwne rzeczy. Np. zamknąć. Albo pobrać z niej dane.

Ten serwer implementuje standard W3C WebDriver. Warto zajrzeć na tę stronę choćby po to, żeby zapoznać się ze wszystkimi możliwościami.

Możesz go zainstalować na kilka sposobów:

  • przez stronę z https://chromedriver.chromium.org/ – to jest zwykły plik exe bez żadnego instalatora. Więc, żeby go wygodnie używać, powinieneś dodać go gdzieś do ścieżki PATH.
  • z użyciem NPM – to coś w rodzaju managera pakietów dla Node.js. Taki NuGet. Jak sprawdzić, czy masz go zainstalowanego? Otwórz konsolę i wpisz npm --version. Jeśli masz zainstalowane, zobaczysz numer wersji. Jeśli nie, to konsola wybuchnie 🙂 Żeby go zainstalować, musisz zainstalować Node.js (możesz to zrobić też z poziomu instalatora Visual Studio). Następnie wystarczy nadusić:
npm install -g chromedriver --detect_chromedriver_version && chromedriver

To polecenie zainstaluje ChromeDriver globalnie i od razu je uruchomi. Później, żeby uruchomić ChromeDriver ponownie (np. po restarcie systemu) wystarczy, że wpiszesz do konsoli samo chromedriver.

  • z użyciem chocolatey – to taki manager pakietów dla Windows. Istnieje kilka innych, np. winget, jednak winget nie ma tego pakietu. Jeśli chcesz ten serwer instalować w taki sposób, przede wszystkim musisz uruchomić konsolę JAKO ADMINISTRATOR, a następnie wpisz: choco install chromedriver. Potem już tylko uruchom, wpisując chromedriver.

Moim subiektywnym zdaniem, instalacja za pomocą npm jest najprostsza. Generalnie instalacja tego za pomocą managera pakietów ma tą przewagę, że one od razu „rejestrują” ten chromedriver globalnie w systemie. Tzn., że żeby go uruchomić wystarczy, że w konsoli wpiszesz chromedriver.

Postman i przeglądarka

Uruchomienie przeglądarki

Gdy już masz URUCHOMIONY chromeDriver (upewnij się, że działa – blokuje okno konsoli), możesz uruchomić PostMana i wysłać swój pierwszy request:

Tak wygląda uruchamianie ChromeDriver

Wyślij metodą POST na adres http://localhost:9515/session takiego jsona:

{
  "capabilities": {
    "alwaysMatch": {
      "browserName": "chrome",
      "acceptInsecureCerts": true,
      "goog:chromeOptions": {
        "w3c": true
      }
    },
    "firstMatch": [
      {}
    ]
  },
  "desiredCapabilities": {
    "browserName": "chrome",
    "acceptInsecureCerts": true
  }
}

Ten request utworzy nową sesję przeglądarki, odpalając ją jednocześnie. Sukces zakończy się odpowiedzią:

{
    "value": {
        "capabilities": {
            "acceptInsecureCerts": true,
            "browserName": "chrome",
            "browserVersion": "105.0.5195.127",
            "chrome": {
                "chromedriverVersion": "105.0.5195.52 (412c95e518836d8a7d97250d62b29c2ae6a26a85-refs/branch-heads/5195@{#853})",
                "userDataDir": "C:\\Users\\Admin\\AppData\\Local\\Temp\\scoped_dir21756_1039460380"
            },
            "goog:chromeOptions": {
                "debuggerAddress": "localhost:59196"
            },
            "networkConnectionEnabled": false,
            "pageLoadStrategy": "normal",
            "platformName": "windows",
            "proxy": {},
            "setWindowRect": true,
            "strictFileInteractability": false,
            "timeouts": {
                "implicit": 0,
                "pageLoad": 300000,
                "script": 30000
            },
            "unhandledPromptBehavior": "dismiss and notify",
            "webauthn:extension:credBlob": true,
            "webauthn:extension:largeBlob": true,
            "webauthn:virtualAuthenticators": true
        },
        "sessionId": "1fb6180770fd83cca794babb47074ad2"
    }
}

Najważniejsze tutaj to sessionId na samym końcu odpowiedzi. To jest identyfikator sesji, którym będziesz się posługiwał w kolejnych żądaniach do tej przeglądarki (tak, możesz mieć wiele sesji i wiele okien przeglądarki).

Wywołanie adresu w przeglądarce

Oczywiście do przeglądarki możesz wysłać dowolny adres, który ona otworzy. Służy do tego końcówka url.

Wyślij POST na http://localhost:9515/session/:sessionId/url, gdzie :sessionId to oczywiście ID konkretnej sesji. W BODY musisz przekazać parametr o nazwie url w postaci json:

{
    "url": "https://www.google.pl"
}

Pobranie adresu z przeglądarki

A teraz pobierzemy adres z paska adresu w otwartej przeglądarce.

Wyślij żądanie metodą GET na ten sam adres, co wyżej: http://localhost:9515/session/:sessionId/url, gdzie :sessionId to oczywiście ID konkretnej sesji. W odpowiedzi dostaniesz takiego JSONa:

{
    "value": "https://www.google.pl/"
}

Możliwości

Możliwości są właściwie ograniczone tylko wyobraźnią. W specyfikacji WebDriver jest wszystko opisane. Możesz minimalizować okno, zamykać, tworzyć nowe karty, pobierać HTML, klikać konkretne elementy na stronie. Właściwie wszystko. I to za pomocą odpowiednich requestów opisanych w ww. specyfikacji. Na koniec zostawiam wisienkę na torcie.

PostMan, przeglądarka i OAuth2

Wyobraź sobie, że pracujesz na WebAPI, które korzysta z innego API. Twoje WebApi wymaga logowania za pomocą OAuth2, np. w użyciem konta Microsoft. W Postmanie taki flow można bardzo łatwo osiągnąć, tworząc kolekcję i konfigurując w niej autoryzację.

Jednak problem jest inny – dodatkowe API, które jest wywoływane przez Twoje API potrzebuje drugiej autoryzacji. I tu wchodzimy w konkretny przypadek użycia. Żeby móc wywołać jakąkolwiek końcówkę z dodatkowego API, musisz najpierw wywołać stronę logowania, pobrać z niej AuthCode, następnie wysłać ten AuthCode do konkretnego endpointa, żeby otrzymać authorization token i refresh token. I dopiero mając te tokeny możesz bawić się z tym drugim API. Najzwyklejszy flow OAuth2.

I niby wszystko masz już podane na tacy – wiesz jak wywołać konkretne requesty do przeglądarki. Problem polega na tym, że skrypty w PostManie działają asynchronicznie. W rezultacie, skończysz z asynchronicznym piekłem – czyli zagnieżdżeniami, gdzie kolejny request musisz wywołać na callback poprzedniego. A na koniec może się okazać, że i tak wysłałeś żądanie tokenów za wcześnie. Pokażę Ci teraz, jak to zrobić w skrypcie PostMana.

Zaznaczam, że nie lubię, nie umiem i nawet chyba nie szanuję JavaScriptu, więc mój kod może nie być idealny, ale działa 😉

Synchroniczne requesty

Tak naprawdę wszystkie requesty wciąż będą asynchroniczne, ale zadziała tutaj słówko await dokładnie w taki sam sposób, jakby się można było tego spodziewać w .NET.

Niemniej jednak na początku musimy napisać kilka helperów.

Aha, jeśli nie wiesz, jak się pisze skrypty w Postmanie, to po prostu kliknij na kartę Pre-Request Script w oknie żądania. Ten skrypt uruchomi się tuż przed wysłaniem faktycznego żądania.

Na początek trzeba zaimportować sdk PostMana, którego będziemy używać:

const sdk = require('postman-collection');

Teraz napiszemy sobie dwie pierwsze metody: waitUntilDone i sleep. Można by to przetłumaczyć na .Net jako Task.When() i Task.Delay():

function waitUntilDone(promise) {
        const wait = setInterval(() => {}, 300000);
        promise.finally(() => clearInterval(wait));
}
 
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

Teraz metoda, która służy do wysyłania requestów. Oczywiście PostMan w swoim obiekcie pm ma taką metodę jak sendRequest. Jednak nasz sendRequest będzie mógł być awaitowany i zwróci wynik w returnie, a nie w callbacku.

Teraz napiszemy funkcję sleep, którą można by przetłumaczyć na .NET jako Task.Delay:

function sendRequest(req) {
    return new Promise((resolve, reject) => {        
        pm.sendRequest(req, (err, res) => {
            if (err) {
                return reject(err);
            }

            return resolve(res);
        })
    });
}

Teraz funkcja, która otworzy okno przeglądarki:

async function sendOpenBrowserRequest()
{
    let openBrowserRequest = {
        url: "http://localhost:9515/session",
        method: "POST",
        body: {
            mode: "raw",
            raw: JSON.stringify({
                    "capabilities": {
                        "alwaysMatch": {
                            "browserName": "chrome",
                            "acceptInsecureCerts": true,
                            "goog:chromeOptions": {
                                "w3c": true
                            }
                        },
                        "firstMatch": [
                            {}
                        ]
                    },
                    "desiredCapabilities": {
                        "browserName": "chrome",
                        "acceptInsecureCerts": true
                    }
                })
        }
    };

    return await sendRequest(openBrowserRequest);
}

Zwróć uwagę, że na początku tej funkcji występuje słówko async. Poza tym nic się tu ciekawego nie dzieje. Po prostu wysyłamy żądanie otwarcia przeglądarki dokładnie takie samo, jak wyżej w tym artykule opisałem.

To teraz funkcja do nawigowania – przechodzimy na konkretną stronę w otwartej przeglądarce.

async function sendNavigateRequest(sessionId)
{
    let urlParam = "https://www.google.pl";

    let bodyParam = {
        "url": urlParam
    };

    let navigateRequest = {
         url: "http://localhost:9515/session/" + sessionId + "/url",
         method: "POST",
         body: {
             mode: "raw",
             raw: JSON.stringify(bodyParam)
         }
     }

    return await sendRequest(navigateRequest);
}

Tutaj też wysyłamy prosty request jak wyżej.

Teraz będzie trochę magii. Funkcja, która pobierze wartość z query parameter w aktualnym adresie z przeglądarki:

async function getCallbackAuthCode(sessionId)
{            
    let getAddressRequest = {
             url: "http://localhost:9515/session/" + sessionId + "/url",
             method: "GET"
         }

    let authCode = "";

    while(authCode == "")
    {
        await sleep(3000);

        const response = await sendRequest(getAddressRequest);
        let data = response.json();

        if(data.value.includes("code="))
        {
            const url = new sdk.Url(data.value);
            authCode = url.query.find((i) => i.key == "code");
        }
    }

    return authCode;
}

Tutaj zasadniczo chodzi o to:

  • pobierz adres z przeglądarki (wysłanie getAddressRequest)
  • jeśli w adresie znajduje się string "code=", wtedy odczytaj query parameter o nazwie „code” i zakończ funkcję
  • jeśli nie ma, to poczekaj 3 sekundy i zobacz znów.

Po co to czekanie? Pamiętaj jak działa OAuth2. Otwierasz przeglądarkę – za pomocą skryptu. Przechodzisz na stronę logowania – za pomocą skryptu. Logujesz się, podając swoje dane – ręcznie. Po zalogowaniu zostajesz przekierowany na jakiegoś callbacka, w którym masz Auth Code. W naszym wypadku założyłem, że ten adres przekierowania będzie zawierał AuthCode w parametrze code.

No i to czekanie to czas pomiędzy tym jak otworzysz stronę logowania i jak zostaniesz przekierowany z poprawnym AuthCodem. Musimy poczekać na to przekierowanie.

Teraz już właściwie koniec – piszemy główny program:

async function main() {
    
    //otwieram przeglądarkę
    let result = await sendOpenBrowserRequest();
    const openBrowserResult = result.json();

    //sprawdzam, czy się udało
    if(openBrowserResult.value.hasOwnProperty('error'))
    {
        console.error("Nie można było otworzyć przeglądarki");
        return;
    }

    //przechodzę na stronę logowania
    let navigateResult = await sendNavigateRequest(openBrowserResult.value.sessionId); 

    //czekam na authcode i pobieram go z adresu
    let authCode = await getCallbackAuthCode(openBrowserResult.value.sessionId);
    console.log("Auth code: " + authCode.value);

    //ustawiam zmienną w parametrze url żądania
    pm.variables.set('authcode', authCode.value);
}

//uruchamiam główną funkcję i czekam na jej zakończenie
util.waitUntilDone(main().catch(console.error));

I teraz cały pic polega na tym, że żądanie z PostMana (to główne, które wywołujesz guzikiem SEND) wyjdzie dopiero wtedy, gdy te wszystkie inne procesy się zakończą. Czyli zmienna authcode zostanie ustawiona. Oto cały kod:

const sdk = require('postman-collection');

function waitUntilDone(promise) {
        const wait = setInterval(() => {}, 300000);
        promise.finally(() => clearInterval(wait));
}
 
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

 
function sendRequest(req) {
    return new Promise((resolve, reject) => {        
        pm.sendRequest(req, (err, res) => {
            if (err) {
                return reject(err);
            }

            return resolve(res);
        })
    });
}

async function sendOpenBrowserRequest()
{
    let openBrowserRequest = {
        url: "http://localhost:9515/session",
        method: "POST",
        body: {
            mode: "raw",
            raw: JSON.stringify({
                    "capabilities": {
                        "alwaysMatch": {
                            "browserName": "chrome",
                            "acceptInsecureCerts": true,
                            "goog:chromeOptions": {
                                "w3c": true
                            }
                        },
                        "firstMatch": [
                            {}
                        ]
                    },
                    "desiredCapabilities": {
                        "browserName": "chrome",
                        "acceptInsecureCerts": true
                    }
                })
        }
    };

    return await sendRequest(openBrowserRequest);
}
 
async function sendNavigateRequest(sessionId)
{
    let urlParam = `https://model.simplysign.webnotarius.pl/idp/oauth2.0/authorize?response_type=code&client_id=${clientId}&redirect_uri=https://api.dev.inperly.cloud/providers/asseco/simplysign/callback&state=${backendId}`;

    let bodyParam = {
        "url": urlParam
    };

    let navigateRequest = {
         url: "http://localhost:9515/session/" + sessionId + "/url",
         method: "POST",
         body: {
             mode: "raw",
             raw: JSON.stringify(bodyParam)
         }
     }

    return await sendRequest(navigateRequest);
}

 

async function getCallbackAuthCode(sessionId)
{            
    let getAddressRequest = {
             url: "http://localhost:9515/session/" + sessionId + "/url",
             method: "GET"
         }

    let authCode = "";

    while(authCode == "")
    {
        await sleep(3000);

        const response = await sendRequest(getAddressRequest);
        let data = response.json();

        if(data.value.includes("code="))
        {
            const url = new sdk.Url(data.value);
            authCode = url.query.find((i) => i.key == "code");
        }
    }

    return authCode;
}

async function main() {
    
    //otwieram przeglądarkę
    let result = await sendOpenBrowserRequest();
    const openBrowserResult = result.json();

    //sprawdzam, czy się udało
    if(openBrowserResult.value.hasOwnProperty('error'))
    {
        console.error("Nie można było otworzyć przeglądarki");
        return;
    }

    //przechodzę na stronę logowania
    let navigateResult = await sendNavigateRequest(openBrowserResult.value.sessionId); 

    //czekam na authcode i pobieram go z adresu
    let authCode = await getCallbackAuthCode(openBrowserResult.value.sessionId);
    console.log("Auth code: " + authCode.value);

    //ustawiam zmienną w parametrze url żądania
    pm.variables.set('authcode', authCode.value);
}

//uruchamiam główną funkcję i czekam na jej zakończenie
util.waitUntilDone(main().catch(console.error));

Testujemy

Możesz to bardzo prosto przetestować. Stwórz żądanie w PostManie, które wywoła taki adres metodą GET:

https://postman-echo.com/get?mycode={{authcode}}

Wywołujemy tutaj specjalne żądanie z usług PostMana, które jest takim „echo”. Tzn. w odpowiedzi dostaniemy m.in. przekazane parametry.

W skrypcie zmienną authcode ustawiamy w tym miejscu: pm.variables.set('authcode', authCode.value);

Więc jeśli teraz uruchomisz żądanie z pełnym skryptem, to na początku zostanie otwarte okno przeglądarki, następnie przeglądarka przejdzie na adres https://www.google.pl i będzie czekać. W tym momencie jesteśmy w funkcji getCallbackAuthCode i skrypt czeka, aż w pasku adresu pojawi się ciąg: „code=„. Możesz go teraz tam dopisać do istniejącego adresu ręcznie, np: https://www.google.pl?code=siema

Teraz, gdy wciśniesz Enter, skrypt rozpozna, że jest tam ciąg „code=" i pobierze jego wartość. Ta wartość zostanie ustawiona jako wartość zmiennej authcode w Twoim żądaniu i ostatecznie PostMan wyśle takie żądanie:

https://postman-echo.com/get?mycode=siema

W odpowiedzi zobaczysz coś takiego:

{
    "args": {
        "mycode": "siema"
    },
    "headers": {
        "x-forwarded-proto": "https",
        "x-forwarded-port": "443",
        "host": "postman-echo.com",
        "x-amzn-trace-id": "Root=1-632c68fd-2cf9a66c589406a350a1b45a",
        "user-agent": "PostmanRuntime/7.29.2",
        "accept": "*/*",
        "cache-control": "no-cache",
        "postman-token": "9a9d743d-f477-41d0-b574-b0842f55d496",
        "accept-encoding": "gzip, deflate, br",
        "cookie": "sails.sid=s%3AhbMvyUvCoQuNBTunE9ub8u3XOACGD2mA.%2FEZrc%2BoGkIPsjtlhQFA082LQNMijJsIhuhwepAv0ZW8"
    },
    "url": "https://postman-echo.com/get?mycode=siema"
}

Życzę miłej zabawy 🙂


To tyle jeśli chodzi o pobieranie danych z przeglądarki przez PostMana. Jeśli temat Cię zainteresował lub potrzebujesz czegoś więcej, koniecznie sprawdź dokumentację standardu WebDriver, z której dowiesz się ile jeszcze rzeczy i w jaki sposób możesz zrobić z przeglądarką za pomocą HTTP.

Dzięki za przeczytanie artykułu. Jeśli znalazłeś w nim jakiś błąd lub czegoś nie rozumiesz, koniecznie daj znać w komentarzu.

Podziel się artykułem na: