Nightwatch.js’te CAPTCHA Nasıl Çözülür (Komut Kuyruğu)

nightwatch captcha - How to Solve CAPTCHAs in Nightwatch.js (Command Queue)

Nightwatch’te bir captcha çözümü, komut kuyruğunun yanında değil, tam olarak kuyruğun içinde çalışmalıdır. Nightwatch, tarayıcı komutlarını onları yazdığınız yerde çalıştırmaz. Komutları kuyruğa alır ve kuyruğu ancak test fonksiyonunuz döndükten sonra boşaltır; bu yüzden iki tarayıcı komutunun arasına yazılan bir çözüm çağrısı anında, yani sayfa yüklenmeden ve widget daha ortada yokken tetiklenir. Çözüm browser.perform, artı yükseltmeniz gereken bir global ayardır; çünkü bir çözüm, Nightwatch’in bir async geri çağırmaya tanıdığı on saniyeden uzun sürer.

Neye ihtiyacınız var

  • Çalışan bir sürücüyle birlikte Nightwatch 3: ya yerelde chromedriver ya da uzak bir WebDriver uç noktası.
  • Bir Windows makinesinde çalışan CapSkip ve testlerinizle aynı projeye kurulmuş Node istemcisi.
  • Sitekey ve sayfa URL’si. Sitekey değerini koda sabitlemek yerine widget üzerinden okuyun; çünkü staging ve production ortamları nadiren aynı sitekey değerini kullanır.
  • Testler çözücünün kendi makinesi dışında bir yerde çalıştığında Server modu; buna her CI runner’ı dahildir. Bu, bağlantı ayarları altındaki tek bir ayardır.
# npm install capskip
npm install --save-dev nightwatch
npm install capskip

Adım 1: düz bir çözüm çağrısı neden çok erken çalışır

Bir Nightwatch testindeki her tarayıcı komutu, bir kuyruğa eklenen talimattır. Önce test fonksiyonu yukarıdan aşağıya çalışıp bu kuyruğu oluşturur, Nightwatch kuyruğu ancak ondan sonra yürütmeye başlar. Aradaki sıradan JavaScript kodu kuyruğun parçası değildir, dolayısıyla kuyruk oluşturulurken çalışır. Tüm sorun tek cümlede bundan ibarettir.

// WRONG. The solve starts while the queue is still being
// built, so it runs before browser.url() has navigated.
module.exports = {
  "signup form": function (browser) {
    browser.url("https://example.com/page-with-recaptcha");

    solver.recaptcha(sitekey, pageUrl).then((r) => {
      // fires first, against a page that is not open yet
    });

    browser.click("#submit");
  },
};

Belirti kafa karıştırıcıdır, çünkü hiçbir hata fırlatılmaz. Çözüm başarılı olur, test bazen geçer ve elinizdeki token hiç gerçekleşmemiş bir sayfa yüklemesine aittir. Nightwatch, kendi kodunuzu bunun yerine kuyruğa koymanız için belgelenmiş bir yol sunar: geri çağırması kuyruğun parçası olarak çalıştırılacak fonksiyon diye tanımlanan browser.perform.

// RIGHT. perform() queues the callback, so it runs in
// sequence with the commands either side of it.
browser.url("https://example.com/page-with-recaptcha");

browser.perform(async function () {
  const { code } = await solver.recaptcha(sitekey, pageUrl);
  return code;
});

browser.click("#submit");

Diğer seçenek async bir testtir. Test fonksiyonunu async olarak tanımlamak API komutlarının promise döndürmesini sağlar; her birini await ile beklemek, perform kullanmadan her şeyi sırada tutar. İkisi de işe yarar. Hata, ikisini karıştırmaktır: await edilmemiş tarayıcı komutlarının arasına yerleşmiş, await edilen harici bir promise sizi doğrudan ilk örneğe geri götürür.

Adım 2: asyncHookTimeout değerini yükseltin, yoksa çözüm on saniyede zaman aşımına uğrar

Bir öğleden sonranızı yiyen madde budur. perform içindeki asenkron yürütme asyncHookTimeout globali ile sınırlıdır ve bunun varsayılanı 10000 milisaniyedir. Bir reCAPTCHA çözümü ise düzenli olarak on beş ila kırk beş saniye sürer. Sonuç: çözücü hâlâ çalışırken geri çağırma öldürülür ve aldığınız hata CAPTCHA’dan değil, bir zaman aşımından söz eder.

Bu değeri Nightwatch yapılandırmanızda, genel olarak veya ortam bazında yükseltin.

// nightwatch.conf.js
module.exports = {
  globals: {
    // Default is 10000, which is shorter than most solves.
    asyncHookTimeout: 120000,

    // waitFor commands default to 5000. The widget is not
    // the slow part, but give it room on a cold CI runner.
    waitForConditionTimeout: 15000,
  },
};

İstemci tarafını bunun altında kalacak şekilde ayarlayın ki hangi üst sınırın geçerli olduğu net olsun. Node istemcisi kendi takvimine göre yoklama yapar: 250 milisaniyeden başlar ve kademeli olarak pollingInterval değerine çıkar; genellikle elle yazılmış bir döngüden daha erken dönmesinin sebebi budur.

// npm install capskip
const { CapSkip } = require("capskip");

const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST || "127.0.0.1",
  port: 8080,
  // Seconds. Keep this under the 120s asyncHookTimeout above.
  recaptchaTimeout: 90,
  pollingInterval: 3,
});

Adım 3: token’ı setValue ile değil, execute ile enjekte edin

reCAPTCHA’nın okuduğu yanıt alanı gizli bir textarea öğesidir. WebDriver, etkileşime kapalı saydığı öğelerle çalışmayı reddeder; bu yüzden bu alanda setValue kullanmak element not interactable hatasıyla başarısız olur. Normal çözüm, değeri sayfa üzerinden enjekte etmektir; Nightwatch bunu execute olarak sunar ve execute bir fonksiyon gövdesi, bir argüman dizisi ve isteğe bağlı bir geri çağırma alır.

browser.perform(async function () {
  const { code } = await solver.recaptcha(sitekey, pageUrl);

  // The function is serialized and run in the page, so it
  // cannot close over anything. Pass values in the array.
  await browser.execute(
    function (token) {
      document.getElementById("g-recaptcha-response").value = token;
    },
    [code]
  );
});

Form, alanı gönderim anında okumak yerine tamamlanınca bir geri çağırma çalıştırıyorsa, o geri çağırmayı da aynı betikte çağırın. Invisible reCAPTCHA neredeyse her zaman böyle çalışır ve widget çeşitleri yalnızca ilettiğiniz seçenekleri değiştirir: invisible veya enterprise 1 olarak ayarlanır, version bir action ile birlikte v3 yapılır ya da recaptcha yerine turnstile ve geetest kullanılır. Seçeneklerin tamamı şurada: Node.js CAPTCHA çözücü sayfası.

Adım 4: testin tamamı

İstemci, testin dışında modül kapsamında bir kez oluşturulur; böylece her test durumu için yeniden kurulmaz. Sitekey koda sabitlenmek yerine sayfadan okunur; aynı spec dosyasının hem staging hem production ortamında çalışmasını sağlayan da budur.

// npm install capskip
const { CapSkip } = require("capskip");

const solver = new CapSkip({
  host: process.env.CAPSKIP_HOST || "127.0.0.1",
  port: 8080,
  recaptchaTimeout: 90,
});

const PAGE = "https://example.com/page-with-recaptcha";

describe("signup", function () {
  it("submits through the reCAPTCHA", async function (browser) {
    await browser.url(PAGE);
    await browser.waitForElementPresent(".g-recaptcha", 15000);

    // Read the sitekey off the widget that is actually there.
    const sitekey = await browser.getAttribute(
      ".g-recaptcha",
      "data-sitekey"
    );

    await browser.perform(async function () {
      const { code } = await solver.recaptcha(sitekey.value, PAGE);
      await browser.execute(
        function (token) {
          document.getElementById("g-recaptcha-response").value = token;
        },
        [code]
      );
    });

    // Submit straight after. The token is not a long-lived value.
    await browser.click("#submit");
    await browser.assert.textContains(".result", "Thanks");
  });
});

Çözümü yapabildiğiniz kadar geç yapın ve hemen gönderin. Bir reCAPTCHA token’ı yaklaşık iki dakika geçerlidir; before kancasında çözüp üç test durumu sonra gönderen bir süit, süresi dolmuş bir token gönderiyor demektir. Bu süreyi bir kez okumakta fayda var: bir reCAPTCHA token’ı ne kadar süre geçerli kalır.

Adım 5: CI üzerinde çalıştırmak ve bunun hangi bağlantı modunu gerektirdiği

Nightwatch testleri nadiren yazıldıkları makinede kalır. Bir CI runner’ına, bir konteynere ya da bir Selenium Grid düğümüne taşındıkları anda loopback artık sizin masanız anlamına gelmez. İki bağlantı modu vardır. Local, 127.0.0.1 adresine bağlanır ve yalnızca o cihaza yanıt verir. Server ise ağ adresinize veya genel IP adresinize bağlanır; böylece bir runner, bir konteyner ya da bir grid düğümü aynı Windows makinesine API üzerinden ulaşabilir. İkisi de şurada yer alır: bağlantı ayarları, Server modu ise yalnızca çözücünün hangi adresi dinlediğini değiştirir. Donanım yine sizin donanımınızdır ve çözüm başına ücretlendirme yine yoktur.

Test sürecinin çalıştığı yerHangi bağlantı modu
Kendi makineniz, yerel chromedriverLocal modu. 127.0.0.1 gerçekten doğru
Ağınızdaki bir derleme ajanıÇözücünün LAN adresiyle Server modu
Barındırılan bir CI runner’ıStatik bir genel IP ve bir güvenlik duvarı kuralı ile Server modu
Bir konteyner, çözücü ana makinedeServer modu. Container içindeki loopback, container’ın kendisidir

Grid üzerinde insanları yanıltan bir ayrım var: çözücü çağrısını tarayıcı değil, test süreci yapar. Dolayısıyla önemli olan adres, Node sürecinin ulaşabildiği adrestir; grid düğümünün ağ yapılandırması bu açıdan konu dışıdır. Bu ayrım şurada ayrıntılı ele alınıyor: Selenium Grid rehberi, ikisinin de altındaki sürücü katmanı ise şurada anlatılıyor: Selenium CAPTCHA çözücü sayfası.

Sık görülen hatalar ve anlamları

GördüğünüzNedenDüzeltme
Çözüm, sayfa yönlendirilmeden önce günlüğe düşüyorÇağrı kuyruğun dışında duruyor, bu yüzden kuyruk oluşturulurken çalışıyorÇağrıyı browser.perform içine alın
Neredeyse tam on saniyede zaman aşımıasyncHookTimeout hâlâ varsayılan 10000 değerindeDeğeri globals içinde, istemci zaman aşımının üzerine çıkarın
Yanıt alanında element not interactable hatasıAlan gizli bir textarea ve WebDriver oraya yazı yazmazDeğeri browser.execute ile atayın
Test geçtiği hâlde token reddediliyorÇözüm ile gönderim arasında süresi dolduÇözümü bir kancada değil, göndermeden hemen önce yapın
Süit CI’ya taşınınca NetworkExceptionÇözücü runner üzerinde değilServer modu ve runner üzerinde CAPSKIP_HOST ayarı
Bir ApiException içinde ERROR_GOOGLEKEYSitekey yanlış widget’tan veya bir iframe URL’sinden alınmışdata-sitekey değerini çözdüğünüz öğeden okuyun
Elle yazılmış bir yoklamadan CAPCHA_NOT_READYSonuç, tamamlanmadan önce okunduYoklamayı istemciye bırakın. Kendiliğinden geri çekilir

Son yanıt tam da göründüğü gibi yazılır; eksik harf bizim tarafımızdaki bir yazım hatası değildir, çünkü API gerçekten de onu bu şekilde döndürür. Tamamı şurada açıklanıyor: CAPCHA_NOT_READY rehberi.

FAQ

Bunun için bir Nightwatch eklentisine ihtiyacım var mı?

Hayır. Çözücü, spec dosyasında require ettiğiniz sıradan bir Node paketidir; yani kaydedilecek özel bir komut ya da plugins dizisine eklenecek bir şey yoktur. Yine de bir eklenti yazmaya kalkarsanız, sarmalanmaya değen tek şey perform ve execute ikilisidir: yaklaşık sekiz satırdır ve bu satırları her spec dosyasında tekrarlamaktan sizi kurtarır.

Genel bir before kancasında bir kez çözüp token’ı tekrar kullanabilir miyim?

Hayır, birbirinden bağımsız iki sebeple. Token yaklaşık iki dakikada geçerliliğini yitirir ve kayda değer büyüklükteki her süit bu süreyi aşar. Ayrıca token, kendisini üreten sayfa yüklemesine bağlıdır; sayfayı yeniden yükleyen ikinci bir test durumunun kendi token’ına ihtiyacı olur. Çözümü her test durumunda, o durumun mümkün olan en geç noktasında yapın. Çözücüde çözüm başına ücretlendirme olmadığı için bunun maliyeti birkaç saniyelik duvar saatinden ibarettir.

Testlerim paralel çalışıyor. Bu bir şeyi değiştirir mi?

Yalnızca aritmetiği. Her worker’ın kendi kuyruğu ve kendi çözüm çağrısı vardır; yani dört worker, eşzamanlı dört çözüm demektir. Koordine edilmesi gereken bir şey yoktur ve hiçbir iş ortak bir bakiyenin arkasında sıraya girmez, çünkü üst sınır bir kredi sayısı değil, CapSkip’i çalıştıran makinedir. Makine yoğun bir günde aynı anda dört çözüm yapıyorsa asyncHookTimeout değerini biraz yükseltin.

Bunu WebdriverIO’da yapmaktan farkı ne?

WebdriverIO komutlarını yazdığınız yerde çözer; bu yüzden iki komut arasına konan bir çözüm çağrısı, hiçbir ek çabaya gerek kalmadan doğru yere oturur. Nightwatch ise komutları kuyruğa alır; perform bu yüzden vardır ve bu yazı bu yüzden ona ayrı bir bölüm ayırır. Token alındıktan sonrası ikisinde de birebir aynıdır. WebdriverIO sürümü şurada adım adım anlatılıyor: WebdriverIO rehberi.

Kısa özet

Çözümü browser.perform içine koyun; çünkü kuyruğun dışındaki her şey siz istediğiniz anda değil, kuyruk oluşturulurken çalışır. asyncHookTimeout değerini istemcinin kendi zaman aşımının üzerine çıkarın; çünkü varsayılan 10000 değeri bir çözümden kısadır. Token’ı execute ile enjekte edin; çünkü yanıt alanı gizlidir. Çözümden hemen sonra gönderin. Testlerin çözücünün kendi makinesinde olmadığı her yerde CAPSKIP_HOST değerini ortamdan alın ve CapSkip’i Server modunda çalıştırın.

Süitteki her spec dosyasına bir çözüm eklemeden önce bilmekte fayda var: CapSkip bir yerel captcha çözücü, dolayısıyla iki yüz kez çözüm yapan bir test koşusu, tek bir kez çözüm yapan koşuyla tam olarak aynı maliyeti taşır.