diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..eae97d1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +userDataDir** +node_modules + +.gitignore +**Dockerfile** +.dockerignore diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3f0507a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +# FROM mcr.microsoft.com/playwright:v1.20.0 +FROM ubuntu:focal + +ARG DEBIAN_FRONTEND=noninteractive + +# Configure Xvfb via environment variables: +ENV SCREEN_WIDTH 1440 +ENV SCREEN_HEIGHT 900 +ENV SCREEN_DEPTH 24 +ENV DISPLAY :60 + +# Configure VNC via environment variables: +ENV VNC_ENABLED true +ENV VNC_PASSWORD secret +ENV VNC_PORT 5900 +EXPOSE 5900 + +# === INSTALL Node.js === + +# Taken from https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal +RUN apt-get update && \ + # Install node16 + apt-get install -y curl wget && \ + curl -sL https://deb.nodesource.com/setup_16.x | bash - && \ + apt-get install -y nodejs && \ + # Feature-parity with node.js base images. + apt-get install -y --no-install-recommends git openssh-client && \ + npm install -g yarn && \ + # clean apt cache + rm -rf /var/lib/apt/lists/* && \ + # Create the pwuser + adduser pwuser + + +# === Install the base requirements to run and debug webdriver implementations === +RUN apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y \ + xvfb \ + xauth \ + ca-certificates \ + x11vnc \ + fluxbox \ + stterm \ + curl \ + tini \ + && apt-get clean \ + && rm -rf \ + /tmp/* \ + /usr/share/doc/* \ + /var/cache/* \ + /var/lib/apt/lists/* \ + /var/tmp/* + + +WORKDIR /fgc +COPY package.json . +# Install chromium & dependencies only +RUN export PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ + && npm install \ + && npx playwright install-deps \ + && npx playwright install chromium \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY . . + +# Shell scripts +RUN mv ./docker/entrypoint.sh /usr/local/bin/entrypoint \ + && chmod +x /usr/local/bin/entrypoint \ + && mv ./docker/vnc-start.sh /usr/local/bin/vnc-start \ + && chmod +x /usr/local/bin/vnc-start + + +ENTRYPOINT ["entrypoint"] +CMD ["node", "epic-games.js"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..6880f75 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +if [ "$VNC_ENABLED" = true ]; then + set -- vnc-start "$@" +fi + +if [ "$EXPOSE_X11" = true ]; then + set -- --listen-tcp "$@" +fi + +# 6000+SERVERNUM is the TCP port Xvfb is listening on: +SERVERNUM=$(echo "$DISPLAY" | sed 's/:\([0-9][0-9]*\).*/\1/') + +# Options passed directly to the Xvfb server: +# -ac disables host-based access control mechanisms +# −screen NUM WxHxD creates the screen and sets its width, height, and depth +SERVERARGS="-ac -screen 0 ${SCREEN_WIDTH}x${SCREEN_HEIGHT}x${SCREEN_DEPTH}" + +exec tini -g -- \ + xvfb-run --server-num "$SERVERNUM" --server-args "$SERVERARGS" "$@" diff --git a/docker/vnc-start.sh b/docker/vnc-start.sh new file mode 100755 index 0000000..10ce6bb --- /dev/null +++ b/docker/vnc-start.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Disable fbsetbg and start fluxbox in a background process: +mkdir -p ~/.fluxbox && echo 'background: unset' >>~/.fluxbox/overlay +fluxbox -display "$DISPLAY" & + +# Start VNC in a background process: +x11vnc -display "$DISPLAY" -forever -shared -rfbport "${VNC_PORT:-5900}" \ + -passwd "${VNC_PASSWORD:-secret}" & + +# Execute the given command: +exec "$@" diff --git a/epic-games.js b/epic-games.js index b098603..b670241 100644 --- a/epic-games.js +++ b/epic-games.js @@ -3,16 +3,17 @@ import path from 'path'; import { __dirname, stealth } from './util.js'; const debug = process.env.PWDEBUG == '1'; // runs non-headless and opens https://playwright.dev/docs/inspector -const URL_LOGIN = 'https://www.epicgames.com/login'; -const URL_CLAIM = 'https://www.epicgames.com/store/en-US/free-games'; +const URL_CLAIM = 'https://store.epicgames.com/store/en-US/free-games'; +const URL_LOGIN = 'https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM; const TIMEOUT = 20 * 1000; // 20s, default is 30s // https://playwright.dev/docs/auth#multi-factor-authentication const context = await chromium.launchPersistentContext(path.resolve(__dirname, 'userDataDir'), { - channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge + // chrome will not work in linux arm64, only chromium + // channel: 'chrome', // https://playwright.dev/docs/browsers#google-chrome--microsoft-edge headless: false, viewport: { width: 1280, height: 1280 }, - userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO update if browser is updated! + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36', // see replace of Headless in util.newStealthContext. TODO update if browser is updated! locale: "en-US", // ignore OS locale to be sure to have english text for locators args: [ // don't want to see bubble 'Restore pages? Chrome didn't shut down correctly.', but flags below don't work. '--disable-session-crashed-bubble', @@ -35,15 +36,15 @@ const clickIfExists = async selector => { await page.click(selector); }; -await page.goto(URL_CLAIM, {waitUntil: 'domcontentloaded'}); // default 'load' takes forever +await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); // default 'load' takes forever // with persistent context the cookie message will only show up the first time, so we can't unconditionally wait for it - try to catch it or let the user click it. await clickIfExists('button:has-text("Accept All Cookies")'); // to not waste screen space in --debug while (await page.locator('a[role="button"]:has-text("Sign In")').count() > 0) { // TODO also check alternative for signed-in state console.error("Not signed in anymore. Please login and then navigate to the 'Free Games' page."); context.setDefaultTimeout(0); // give user time to log in without timeout - await page.goto(URL_LOGIN, {waitUntil: 'domcontentloaded'}); + await page.goto(URL_LOGIN, { waitUntil: 'domcontentloaded' }); // after login it just reloads the login page... - await page.waitForNavigation({url: URL_CLAIM}); + await page.waitForNavigation({ url: URL_CLAIM }); context.setDefaultTimeout(TIMEOUT); // process.exit(1); } @@ -54,7 +55,7 @@ await page.waitForSelector(game_sel); // const games = await page.$$(game_sel); // 'Element is not attached to the DOM' after navigation; had `for (const game of games) { await game.click(); ... } const n = await page.locator(game_sel).count(); console.log('Number of free games:', n); -for (let i=1; i<=n; i++) { +for (let i = 1; i <= n; i++) { await page.click(`:nth-match(${game_sel}, ${i})`); const title = await page.locator('h1 div').first().innerText(); console.log('Current free game:', title); @@ -105,8 +106,8 @@ for (let i=1; i<=n; i++) { } // await page.pause(); } - if (i