Compare commits
9 Commits
c8111c6e48
...
48708af68e
Author | SHA1 | Date |
---|---|---|
|
48708af68e | |
|
3ff18d1042 | |
|
0f9b3a5ac5 | |
|
e4742f1c6e | |
|
76cb7c7958 | |
|
bef7206642 | |
|
28fe49c2b2 | |
|
9f113d6a3f | |
|
8de0d93eb1 |
|
@ -1,5 +1,8 @@
|
||||||
*
|
*
|
||||||
!.git
|
!.git
|
||||||
|
!chase2fa.py
|
||||||
|
!entrypoint.sh
|
||||||
!pinentry-stub.sh
|
!pinentry-stub.sh
|
||||||
!pyproject.toml
|
!pyproject.toml
|
||||||
|
!secretsocket.py
|
||||||
!xactfetch.py
|
!xactfetch.py
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM registry.fedoraproject.org/fedora-minimal:38 AS build
|
FROM git.pyrocufflink.net/containerimages/dch-base AS build
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/var/cache \
|
RUN --mount=type=cache,target=/var/cache \
|
||||||
microdnf install -y \
|
microdnf install -y \
|
||||||
|
@ -18,11 +18,22 @@ RUN --mount=type=cache,target=/var/cache \
|
||||||
python3-wheel \
|
python3-wheel \
|
||||||
&& :
|
&& :
|
||||||
|
|
||||||
COPY . /src
|
COPY .git /src/.git
|
||||||
|
COPY xactfetch.py pyproject.toml /src
|
||||||
|
|
||||||
RUN python3 -m pip wheel -w /wheels /src
|
RUN python3 -m pip wheel -w /wheels /src
|
||||||
|
|
||||||
FROM registry.fedoraproject.org/fedora-minimal:38
|
|
||||||
|
FROM scratch AS mixin
|
||||||
|
|
||||||
|
COPY pinentry-stub.sh /usr/local/bin/pinentry-stub
|
||||||
|
COPY secretsocket.py /usr/local/bin/secretsocket
|
||||||
|
COPY chase2fa.py /usr/local/bin/chase2fa
|
||||||
|
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
|
||||||
|
FROM git.pyrocufflink.net/containerimages/dch-base
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/var/cache \
|
RUN --mount=type=cache,target=/var/cache \
|
||||||
microdnf install -y \
|
microdnf install -y \
|
||||||
|
@ -47,11 +58,16 @@ RUN --mount=type=cache,target=/var/cache \
|
||||||
libXrandr \
|
libXrandr \
|
||||||
libXrender \
|
libXrender \
|
||||||
libXtst \
|
libXtst \
|
||||||
|
libdrm \
|
||||||
libxcb \
|
libxcb \
|
||||||
|
mesa-libgbm \
|
||||||
|
nspr \
|
||||||
|
nss \
|
||||||
pango \
|
pango \
|
||||||
python3 \
|
python3 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
tini \
|
tini \
|
||||||
|
xorg-x11-server-Xvfb \
|
||||||
&& echo xactfetch:x:2468: >> /etc/group \
|
&& echo xactfetch:x:2468: >> /etc/group \
|
||||||
&& echo xactfetch:*:2468:2468:xactfetch:/var/lib/xactfetch:/sbin/nologin >> /etc/passwd \
|
&& echo xactfetch:*:2468:2468:xactfetch:/var/lib/xactfetch:/sbin/nologin >> /etc/passwd \
|
||||||
&& :
|
&& :
|
||||||
|
@ -61,16 +77,15 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/playwright/browsers
|
||||||
RUN --mount=type=bind,from=build,source=/,target=/build \
|
RUN --mount=type=bind,from=build,source=/,target=/build \
|
||||||
python3 -m pip install --no-index -f /build/wheels xactfetch \
|
python3 -m pip install --no-index -f /build/wheels xactfetch \
|
||||||
&& cp /build/root/.cargo/bin/rbw* /usr/local/bin/ \
|
&& cp /build/root/.cargo/bin/rbw* /usr/local/bin/ \
|
||||||
&& install /build/src/pinentry-stub.sh /usr/local/bin/pinentry-stub \
|
&& playwright install chromium \
|
||||||
&& playwright install firefox \
|
|
||||||
&& :
|
&& :
|
||||||
|
|
||||||
|
COPY --from=mixin / /
|
||||||
|
|
||||||
VOLUME /var/lib/xactfetch
|
VOLUME /var/lib/xactfetch
|
||||||
|
|
||||||
WORKDIR /var/lib/xactfetch
|
WORKDIR /var/lib/xactfetch
|
||||||
|
|
||||||
USER 2468:2468
|
USER 2468:2468
|
||||||
|
|
||||||
ENV XDG_CONFIG_HOME=/etc
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
ENTRYPOINT ["tini", "xactfetch", "--"]
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
stream = httpx.stream(
|
||||||
|
'GET',
|
||||||
|
'https://ntfy.pyrocufflink.blue/chase2fa/raw',
|
||||||
|
timeout=httpx.Timeout(5, read=None),
|
||||||
|
)
|
||||||
|
with stream as r:
|
||||||
|
for line in r.iter_lines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
m = re.search(r'\d{4,}', line)
|
||||||
|
if m:
|
||||||
|
print(m.group(0))
|
||||||
|
break
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ $$ -eq 1 ]; then
|
||||||
|
exec tini "$0" -- "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${SECRET_SOCKET_PATH}" ] || [ ! -e "${SECRET_SOCKET_PATH}" ]; then
|
||||||
|
export SECRET_SOCKET_PATH="${SECRET_SOCKET_PATH:-/tmp/.secretsocket}"
|
||||||
|
secretsocket &
|
||||||
|
sspid=$!
|
||||||
|
fi
|
||||||
|
|
||||||
|
xvfb-run -e /dev/stderr -s '-screen 0 1920x1080x24 -nolisten unix' xactfetch "$@"
|
||||||
|
rc=$?
|
||||||
|
|
||||||
|
if [ -n "${sspid}" ]; then
|
||||||
|
kill $sspid
|
||||||
|
fi
|
||||||
|
exit $rc
|
|
@ -15,7 +15,8 @@ log = logging.getLogger('secretsocket')
|
||||||
|
|
||||||
|
|
||||||
ALLOW_UNKNOWN_PEER = os.environ.get('ALLOW_UNKNOWN_PEER') == '1'
|
ALLOW_UNKNOWN_PEER = os.environ.get('ALLOW_UNKNOWN_PEER') == '1'
|
||||||
XDG_RUNTIME_DIR = Path(os.environ['XDG_RUNTIME_DIR'])
|
SECRET_SOCKET_PATH = os.environ.get('SECRET_SOCKET_PATH')
|
||||||
|
XDG_RUNTIME_DIR = os.environ.get('XDG_RUNTIME_DIR')
|
||||||
|
|
||||||
|
|
||||||
class Secret:
|
class Secret:
|
||||||
|
@ -176,7 +177,12 @@ def shutdown(signum, server):
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
sock_path = XDG_RUNTIME_DIR / 'secretsocket/.ss'
|
if SECRET_SOCKET_PATH:
|
||||||
|
sock_path = Path(SECRET_SOCKET_PATH)
|
||||||
|
elif XDG_RUNTIME_DIR:
|
||||||
|
sock_path = Path(XDG_RUNTIME_DIR) / 'secretsocket/.ss'
|
||||||
|
else:
|
||||||
|
sock_path = Path('/tmp/.secretsocket')
|
||||||
|
|
||||||
if not sock_path.parent.exists():
|
if not sock_path.parent.exists():
|
||||||
sock_path.parent.mkdir()
|
sock_path.parent.mkdir()
|
||||||
|
|
80
xactfetch.py
80
xactfetch.py
|
@ -13,7 +13,7 @@ from types import TracebackType
|
||||||
from typing import Any, Optional, Type
|
from typing import Any, Optional, Type
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from playwright.async_api import Page
|
from playwright.async_api import Playwright, Page
|
||||||
from playwright.async_api import async_playwright
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
|
||||||
|
@ -551,11 +551,8 @@ class Chase:
|
||||||
with self.saved_cookies.open(encoding='utf-8') as f:
|
with self.saved_cookies.open(encoding='utf-8') as f:
|
||||||
cookies = await asyncio.to_thread(json.load, f)
|
cookies = await asyncio.to_thread(json.load, f)
|
||||||
await self.page.context.add_cookies(cookies)
|
await self.page.context.add_cookies(cookies)
|
||||||
except:
|
except Exception as e:
|
||||||
log.warning(
|
log.debug('Failed to load saved cookies: %s', e)
|
||||||
'Could not load saved cookies, '
|
|
||||||
'SMS verification will be required!'
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
log.info('Successfully loaded saved cookies')
|
log.info('Successfully loaded saved cookies')
|
||||||
|
|
||||||
|
@ -587,9 +584,39 @@ class Chase:
|
||||||
await logonbox.get_by_role('button', name='Sign in').click()
|
await logonbox.get_by_role('button', name='Sign in').click()
|
||||||
log.debug('Waiting for page load')
|
log.debug('Waiting for page load')
|
||||||
await self.page.wait_for_load_state()
|
await self.page.wait_for_load_state()
|
||||||
await self.page.get_by_role('button', name='Pay Card').wait_for(
|
logonframe = self.page.frame_locator('iframe[title="logon"]')
|
||||||
timeout=120000
|
t_2fa = asyncio.create_task(
|
||||||
|
logonframe.get_by_role(
|
||||||
|
'heading', name="We don't recognize this device"
|
||||||
|
).wait_for()
|
||||||
)
|
)
|
||||||
|
t_finished = asyncio.create_task(
|
||||||
|
self.page.get_by_role('button', name='Pay Card').wait_for()
|
||||||
|
)
|
||||||
|
done, pending = await asyncio.wait(
|
||||||
|
(t_2fa, t_finished),
|
||||||
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
|
)
|
||||||
|
for t in pending:
|
||||||
|
t.cancel()
|
||||||
|
for t in done:
|
||||||
|
await t
|
||||||
|
if t_2fa in done:
|
||||||
|
log.warning('Device verification (SMS 2-factor auth) required')
|
||||||
|
await logonframe.get_by_label('Tell us how: Choose one').click()
|
||||||
|
await logonframe.locator(
|
||||||
|
'#container-1-simplerAuth-dropdownoptions-styledselect'
|
||||||
|
).click()
|
||||||
|
otp_task = asyncio.create_task(self.get_secret('bank.chase.otp'))
|
||||||
|
await logonframe.get_by_role('button', name='Next').click()
|
||||||
|
log.info('Waiting for SMS verification code')
|
||||||
|
otp = await otp_task
|
||||||
|
log.debug('Filling verification code form')
|
||||||
|
await logonframe.get_by_label('One-time code').fill(otp)
|
||||||
|
await logonframe.get_by_label('Password').fill(password)
|
||||||
|
await logonframe.get_by_role('button', name='Next').click()
|
||||||
|
await self.page.wait_for_load_state()
|
||||||
|
await self.page.get_by_role('button', name='Pay Card').wait_for()
|
||||||
log.info('Successfully logged in to Chase')
|
log.info('Successfully logged in to Chase')
|
||||||
self._logged_in = True
|
self._logged_in = True
|
||||||
|
|
||||||
|
@ -675,10 +702,7 @@ class Chase:
|
||||||
return secret.decode()
|
return secret.decode()
|
||||||
|
|
||||||
|
|
||||||
async def amain() -> None:
|
async def fetch_transactions(pw: Playwright, secrets: SecretsClient) -> bool:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
secrets = SecretsClient()
|
|
||||||
await secrets.connect()
|
|
||||||
log.debug('Getting Firefly III access token')
|
log.debug('Getting Firefly III access token')
|
||||||
token = (await secrets.get_secret('firefly.token')).decode()
|
token = (await secrets.get_secret('firefly.token')).decode()
|
||||||
import_secret = (
|
import_secret = (
|
||||||
|
@ -693,10 +717,10 @@ async def amain() -> None:
|
||||||
)
|
)
|
||||||
end_date = datetime.date.today() - datetime.timedelta(days=1)
|
end_date = datetime.date.today() - datetime.timedelta(days=1)
|
||||||
failed = False
|
failed = False
|
||||||
async with async_playwright() as pw, secrets:
|
browser = await pw.chromium.launch(headless=False)
|
||||||
headless = os.environ.get('DEBUG_HEADLESS_BROWSER', '1') == '1'
|
context = await browser.new_context()
|
||||||
browser = await pw.firefox.launch(headless=headless)
|
await context.tracing.start(screenshots=True, snapshots=True)
|
||||||
page = await browser.new_page()
|
page = await context.new_page()
|
||||||
banks = sys.argv[1:] or list(ACCOUNTS.keys())
|
banks = sys.argv[1:] or list(ACCOUNTS.keys())
|
||||||
if 'commerce' in banks:
|
if 'commerce' in banks:
|
||||||
if not await download_commerce(
|
if not await download_commerce(
|
||||||
|
@ -704,11 +728,29 @@ async def amain() -> None:
|
||||||
):
|
):
|
||||||
failed = True
|
failed = True
|
||||||
if 'chase' in banks:
|
if 'chase' in banks:
|
||||||
if not await download_chase(
|
if not await download_chase(page, secrets, end_date, token, importer):
|
||||||
page, secrets, end_date, token, importer
|
|
||||||
):
|
|
||||||
failed = True
|
failed = True
|
||||||
|
if failed:
|
||||||
|
await context.tracing.stop(path='trace.zip')
|
||||||
|
with open('trace.zip', 'rb') as f:
|
||||||
|
await ntfy(
|
||||||
|
'Downloading one or more transaction lists failed.',
|
||||||
|
attach=f.read(),
|
||||||
|
filename='trace.zip',
|
||||||
|
)
|
||||||
|
return failed
|
||||||
|
|
||||||
|
|
||||||
|
async def amain() -> None:
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
async with SecretsClient() as secrets:
|
||||||
|
try:
|
||||||
|
async with async_playwright() as pw:
|
||||||
|
failed = await fetch_transactions(pw, secrets)
|
||||||
raise SystemExit(1 if failed else 0)
|
raise SystemExit(1 if failed else 0)
|
||||||
|
except asyncio.exceptions.InvalidStateError:
|
||||||
|
log.debug('Ignoring exception: %s', exc_info=sys.exc_info())
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
Loading…
Reference in New Issue