about summary refs log tree commit diff
path: root/pkgs/games/humblebundle
diff options
context:
space:
mode:
authoraszlig <aszlig@redmoonstudios.org>2017-06-09 05:04:59 +0200
committeraszlig <aszlig@redmoonstudios.org>2017-06-09 05:04:59 +0200
commit4ab1416c9ded3ef3bec0618e1f7dd838d09f97c7 (patch)
treed173133c438bcd07eacb8d36941e46f0803f3d34 /pkgs/games/humblebundle
parent49eecfdb966eb02efa5b370cb231eb2fe85a3541 (diff)
humble-bundle: Add helper for solving captchas
This is not only a major annoyance for us but seems to bother a few
other people using the humblebundle-python API:

  * saik0/humblebundle-python#11
  * saik0/humblebundle-python#14
  * saik0/humblebundle-python#15

While digging through the reCaptcha2 API and also the implementation of
the Humble Bundle site, I stumbled over this code from
https://www.humblebundle.com/user/captcha:

  var captcha = new Recaptcha2('captcha-holder');
  $('input[type=submit]').click(function(e){
    e.preventDefault();
    // recaptcha v2 only cares about response, but we can let the Android app interface stay the same
    var challenge = '';
    var response = captcha.get_response();
    var android_defined = false;
    if (typeof Android != 'undefined') {
      Android.setCaptchaResponse(challenge, response);
    }
  })

So we only need the response, which we do now using a very ugly written
Qt 5 QWebEngine GUI which we use to ask the user to solve the captcha.

Combined with our downloader, it works like this:

Whenever the login fails with a HumbleCaptchaException, we print a
message with the store path to the GUI helper. We're inside a
fixed-output derivation builder, so we do have networking.

The GUI helper also runs a small TCP server listening on port 18123 and
the downloader inside the Nix builder constantly tries to connect to
that port and waits until it gets just one string (the connection is
directly closed afterwards and the GUI helper exits), which is the
response. This is then passed as recaptcha_response keyword argument to
the login() method of the HumbleApi object.

Signed-off-by: aszlig <aszlig@redmoonstudios.org>
Diffstat (limited to 'pkgs/games/humblebundle')
-rw-r--r--pkgs/games/humblebundle/fetch-humble-bundle/default.nix98
1 files changed, 96 insertions, 2 deletions
diff --git a/pkgs/games/humblebundle/fetch-humble-bundle/default.nix b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
index a27138d8..1249b56e 100644
--- a/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
+++ b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
@@ -1,12 +1,83 @@
 { stdenv, curl, cacert, writeText, fetchFromGitHub, fetchpatch
 , python, pythonPackages
 
+# Dependencies for the captcha solver
+, pkgconfig, qt5, runCommandCC
+
 , email, password
 }:
 
 { name ? null, machineName, downloadName ? "Download", suffix ? "humblebundle", md5 }: let
   cafile = "${cacert}/etc/ssl/certs/ca-bundle.crt";
 
+  getCaptcha = let
+    injectedJS = ''
+      function waitForResponse() {
+        var response = captcha.get_response();
+        if (response != "")
+          document.title = response;
+        else
+          setTimeout(waitForResponse, 50);
+      }
+
+      waitForResponse();
+    '';
+
+    escapeCString = stdenv.lib.replaceStrings ["\"" "\n"] ["\\\"" "\\n"];
+
+    application = writeText "captcha.cc" ''
+      #include <QApplication>
+      #include <QWebEngineView>
+      #include <QTcpServer>
+
+      int main(int argc, char **argv) {
+        QApplication *app = new QApplication(argc, argv);
+        QTcpServer *server = new QTcpServer();
+        QWebEngineView *browser = new QWebEngineView();
+
+        if (!server->listen(QHostAddress::LocalHost, 18123)) {
+          qCritical() << "Unable to listen on port 18123!";
+          return 1;
+        }
+
+        qInfo() << "Waiting for connection from the HB downloader...";
+        if (!server->waitForNewConnection(-1)) {
+          qCritical() << "Unable to accept the connection!";
+          return 1;
+        }
+        qInfo() << "Connection established, spawning window to solve captcha.";
+
+        QTcpSocket *sock = server->nextPendingConnection();
+
+        browser->load(QUrl("https://www.humblebundle.com/user/captcha"));
+        browser->show();
+
+        browser->connect(browser, &QWebEngineView::loadFinished, [=]() {
+          browser->page()->runJavaScript("${escapeCString injectedJS}");
+          browser->connect(
+            browser, &QWebEngineView::titleChanged, [=](const QString &title) {
+              sock->write(title.toUtf8());
+              sock->flush();
+              sock->waitForBytesWritten();
+              sock->close();
+              server->close();
+              app->quit();
+            }
+          );
+        });
+
+        return app->exec();
+      }
+    '';
+
+  in runCommandCC "get-captcha" {
+    nativeBuildInputs = [ pkgconfig ];
+    buildInputs = [ qt5.qtbase qt5.qtwebengine ];
+  } ''
+    g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets) \
+      -Wall -std=c++11 -o "$out" ${application}
+  '';
+
   humbleAPI = pythonPackages.buildPythonPackage rec {
     name = "humblebundle-${version}";
     version = "0.1.1";
@@ -24,7 +95,7 @@
   pyStr = str: "'${stdenv.lib.escape ["'" "\\"] str}'";
 
   getDownloadURL = writeText "gethburl.py" ''
-    import sys, humblebundle
+    import socket, sys, time, humblebundle
 
     def get_products(client):
       gamekeys = client.get_gamekeys()
@@ -51,8 +122,31 @@
             print >>sys.stderr, "  " + ds.name
           raise SystemExit(1)
 
+    def login_with_captcha(hb):
+      print >>sys.stderr, "Solving a captcha is required to log in."
+      print >>sys.stderr, "Please run " ${pyStr (toString getCaptcha)} " now."
+      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      print >>sys.stderr, "Waiting for connection",
+      i = 0
+      while sock.connect_ex(("127.0.0.1", 18123)) != 0:
+        time.sleep(0.1)
+        if i % 10 == 0:
+          sys.stderr.write('.')
+          sys.stderr.flush()
+        i += 1
+      print >>sys.stderr, " connected."
+      print >>sys.stderr, "Waiting for captcha to be solved..."
+      response = sock.recv(4096)
+      sock.close()
+      print >>sys.stderr, "Captcha solved correctly, logging in."
+      hb.login(${pyStr email}, ${pyStr password}, recaptcha_response=response)
+
     hb = humblebundle.HumbleApi()
-    hb.login(${pyStr email}, ${pyStr password})
+    try:
+      hb.login(${pyStr email}, ${pyStr password})
+    except humblebundle.exceptions.HumbleCaptchaException:
+      login_with_captcha(hb)
+
     products = dict(get_products(hb))
     dstruct = find_download(products)