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 dlaNode.js
. Taki NuGet. Jak sprawdzić, czy masz go zainstalowanego? Otwórz konsolę i wpisznpm --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ącchromedriver
.
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:
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 odczytajquery 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.