An Android sign-in driver (ios#7) #203
Executable
+180
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sign IN on a booted Android emulator, through the app's own UI.
|
||||||
|
#
|
||||||
|
# FN_PASS=… Scripts/android-signin.sh relay.flamenet.io fnreviewbeta
|
||||||
|
#
|
||||||
|
# The sibling of `android-signup.sh`, and it exists for the same reason: the
|
||||||
|
# Android half of a cross-platform check has to be driven by something, and
|
||||||
|
# driving it by hand is where the mistakes live. Sign-IN specifically, because
|
||||||
|
# signing up on the live relay leaves a real account behind and spends the
|
||||||
|
# account service's three-per-hour-per-IP budget.
|
||||||
|
#
|
||||||
|
# 🔴 **Three traps, all of which cost me an hour on 2026-09-05, all encoded
|
||||||
|
# below. None is obvious and every one fails SILENTLY.**
|
||||||
|
#
|
||||||
|
# 1. `input text` drops characters on longer strings. A 21-character
|
||||||
|
# passphrase arrived as 11. Nothing reports it; the sign-in simply fails
|
||||||
|
# as though the password were wrong. So: chunks, and read back the count.
|
||||||
|
#
|
||||||
|
# 2. The soft keyboard covers the button. A tap at the button's coordinates
|
||||||
|
# lands on a KEY instead — which types a character into the field you just
|
||||||
|
# filled, so the passphrase grows by one every time you "press" sign in.
|
||||||
|
# So: dismiss the keyboard first, and verify the field afterwards.
|
||||||
|
#
|
||||||
|
# 3. Dismissing the keyboard MOVES the button. Coordinates read before the
|
||||||
|
# dismissal are stale by the time you tap them, and the tap lands on
|
||||||
|
# nothing at all — no error, no network, no clue. So: re-dump immediately
|
||||||
|
# before every tap, never reuse a position across an interaction.
|
||||||
|
#
|
||||||
|
# ⚠️ Prints FAIL with a reason, never a bare non-zero. A driver that fails
|
||||||
|
# quietly is worse than no driver: the run continues against a screen nobody
|
||||||
|
# looked at.
|
||||||
|
set -uo pipefail
|
||||||
|
ADB=~/Library/Android/sdk/platform-tools/adb
|
||||||
|
HOST="${1:?usage: android-signin.sh <host> <handle>}"
|
||||||
|
HANDLE="${2:?usage: android-signin.sh <host> <handle>}"
|
||||||
|
# 🔴 Not in the repo, and not defaulted. This repository is public, and a
|
||||||
|
# default here would be a live passphrase anybody can read (ios#62).
|
||||||
|
PASS="${FN_PASS:?set FN_PASS}"
|
||||||
|
PKG=io.flamenet.messenger
|
||||||
|
|
||||||
|
dump() { $ADB shell uiautomator dump /sdcard/s.xml >/dev/null 2>&1; $ADB shell cat /sdcard/s.xml 2>/dev/null; }
|
||||||
|
|
||||||
|
# Centre of the first node whose text matches, from a FRESH dump. Trap 3.
|
||||||
|
tap_text() {
|
||||||
|
local want="$1" d b x y
|
||||||
|
d=$(dump)
|
||||||
|
b=$(printf '%s' "$d" | tr '<' '\n<' | grep "text=\"$want\"" \
|
||||||
|
| grep -oE 'bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' | head -1)
|
||||||
|
[ -z "$b" ] && return 1
|
||||||
|
x=$(printf '%s' "$b" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\1 \3/' | awk '{print int(($1+$2)/2)}')
|
||||||
|
y=$(printf '%s' "$b" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\2 \4/' | awk '{print int(($1+$2)/2)}')
|
||||||
|
$ADB shell input tap "$x" "$y" >/dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tap the control carrying a given label, and refuse to tap a moving target.
|
||||||
|
#
|
||||||
|
# 🔴 **Trap 3, in its worst form.** A fresh dump is not enough on a slow
|
||||||
|
# emulator: the layout can still be settling when the tap is issued, and the
|
||||||
|
# tap then lands on whatever slid into that position. That is not theoretical —
|
||||||
|
# it put this driver on the SIGN-UP form, one field away from creating an
|
||||||
|
# account on the production relay, while every coordinate it had read was
|
||||||
|
# correct at the moment it read them.
|
||||||
|
#
|
||||||
|
# So: read the bounds twice, a second apart, and only tap when they agree.
|
||||||
|
# A control that is still moving is a control this driver refuses to touch.
|
||||||
|
tap_label() {
|
||||||
|
local want="$1" a b x y
|
||||||
|
bounds_of() {
|
||||||
|
dump | tr '<' '\n<' | grep "text=\"$1\"" \
|
||||||
|
| grep -oE 'bounds="\[[0-9]+,[0-9]+\]\[[0-9]+,[0-9]+\]"' | head -1
|
||||||
|
}
|
||||||
|
a=$(bounds_of "$want"); sleep 1; b=$(bounds_of "$want")
|
||||||
|
[ -z "$a" ] && { echo "FAIL '$want' is not on screen"; return 1; }
|
||||||
|
[ "$a" != "$b" ] && { echo "FAIL '$want' is still moving — refusing to tap"; return 1; }
|
||||||
|
x=$(printf '%s' "$a" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\1 \3/' | awk '{print int(($1+$2)/2)}')
|
||||||
|
y=$(printf '%s' "$a" | sed -E 's/.*\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\].*/\2 \4/' | awk '{print int(($1+$2)/2)}')
|
||||||
|
$ADB shell input tap "$x" "$y" >/dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Kept for screens whose control has no label of its own.
|
||||||
|
tap_button() {
|
||||||
|
local index="$1" d
|
||||||
|
d=$(dump)
|
||||||
|
printf '%s' "$d" > /tmp/fn-andui.xml
|
||||||
|
local xy
|
||||||
|
xy=$(python3 - "$index" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
idx = int(sys.argv[1])
|
||||||
|
s = open('/tmp/fn-andui.xml', encoding='utf-8', errors='replace').read()
|
||||||
|
hits = []
|
||||||
|
for n in re.findall(r'<node[^>]*>', s):
|
||||||
|
if 'clickable="true"' not in n or 'EditText' in n:
|
||||||
|
continue
|
||||||
|
b = re.search(r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', n)
|
||||||
|
if b:
|
||||||
|
hits.append(((int(b.group(1)) + int(b.group(3))) // 2,
|
||||||
|
(int(b.group(2)) + int(b.group(4))) // 2))
|
||||||
|
print(f"{hits[idx][0]} {hits[idx][1]}" if idx < len(hits) else "")
|
||||||
|
PY
|
||||||
|
)
|
||||||
|
[ -z "$xy" ] && return 1
|
||||||
|
$ADB shell input tap $xy >/dev/null 2>&1
|
||||||
|
sleep 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# How many characters are actually in the passphrase field. Trap 1.
|
||||||
|
dots() {
|
||||||
|
dump > /tmp/fn-andui.xml
|
||||||
|
python3 -c "
|
||||||
|
import re
|
||||||
|
s = open('/tmp/fn-andui.xml', encoding='utf-8', errors='replace').read()
|
||||||
|
m = re.findall(r'text=\"(•+)\"', s)
|
||||||
|
print(len(m[0]) if m else 0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Type into the field at a text label, in chunks, verified. Traps 1 and 3.
|
||||||
|
type_into() { # label, text, verify-substring
|
||||||
|
local label="$1" text="$2" want="$3"
|
||||||
|
tap_text "$label" || { echo "FAIL no '$label' field on screen"; exit 1; }
|
||||||
|
$ADB shell input keyevent KEYCODE_MOVE_END >/dev/null 2>&1
|
||||||
|
for _ in $(seq 1 40); do $ADB shell input keyevent 67 >/dev/null 2>&1; done
|
||||||
|
local i=0 len=${#text}
|
||||||
|
while [ "$i" -lt "$len" ]; do
|
||||||
|
$ADB shell input text "${text:$i:7}" >/dev/null 2>&1
|
||||||
|
i=$((i + 7)); sleep 1
|
||||||
|
done
|
||||||
|
sleep 1
|
||||||
|
dump | grep -q "$want" || { echo "FAIL '$label' did not read back as expected"; exit 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
$ADB shell am force-stop "$PKG" >/dev/null 2>&1
|
||||||
|
$ADB shell pm clear "$PKG" >/dev/null 2>&1
|
||||||
|
$ADB shell monkey -p "$PKG" -c android.intent.category.LAUNCHER 1 >/dev/null 2>&1
|
||||||
|
sleep 12
|
||||||
|
|
||||||
|
# Runtime permission prompts, if the image asks.
|
||||||
|
for _ in 1 2 3; do tap_text "Allow" || break; done
|
||||||
|
|
||||||
|
D=$(dump)
|
||||||
|
printf '%s' "$D" | grep -q 'text="Handle"' || { echo "FAIL no sign-in screen"; exit 1; }
|
||||||
|
|
||||||
|
type_into "Server" "$HOST" "$HOST"
|
||||||
|
type_into "Handle" "$HANDLE" "$HANDLE"
|
||||||
|
|
||||||
|
# The passphrase, verified by LENGTH — its text is bullets, so there is
|
||||||
|
# nothing else to compare against.
|
||||||
|
tap_text "Passphrase" || { echo "FAIL no passphrase field"; exit 1; }
|
||||||
|
$ADB shell input keyevent KEYCODE_MOVE_END >/dev/null 2>&1
|
||||||
|
for _ in $(seq 1 40); do $ADB shell input keyevent 67 >/dev/null 2>&1; done
|
||||||
|
i=0
|
||||||
|
while [ "$i" -lt "${#PASS}" ]; do
|
||||||
|
$ADB shell input text "${PASS:$i:7}" >/dev/null 2>&1
|
||||||
|
i=$((i + 7)); sleep 1
|
||||||
|
done
|
||||||
|
sleep 1
|
||||||
|
got=$(dots)
|
||||||
|
[ "$got" = "${#PASS}" ] || { echo "FAIL passphrase arrived as $got of ${#PASS} characters"; exit 1; }
|
||||||
|
|
||||||
|
# Trap 2, then trap 3: put the keyboard away, let the layout settle, and only
|
||||||
|
# then find the button.
|
||||||
|
$ADB shell input keyevent 4 >/dev/null 2>&1
|
||||||
|
sleep 3
|
||||||
|
after=$(dots)
|
||||||
|
[ "$after" = "${#PASS}" ] || { echo "FAIL dismissing the keyboard changed the field ($after chars)"; exit 1; }
|
||||||
|
|
||||||
|
# ⚠️ By LABEL, never by index. An index picked "Create an account" here and
|
||||||
|
# walked onto the sign-up form on the live relay.
|
||||||
|
tap_label "Sign in" || exit 1
|
||||||
|
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
D=$(dump)
|
||||||
|
printf '%s' "$D" | grep -qE 'text="(Contacts|Messages|Chats)"' && { echo "$HOST $HANDLE OK"; exit 0; }
|
||||||
|
printf '%s' "$D" | grep -qi "too many" && { echo "$HOST $HANDLE SKIP throttled"; exit 0; }
|
||||||
|
printf '%s' "$D" | grep -qi "check your" && { echo "$HOST $HANDLE FAIL credentials refused"; exit 1; }
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
echo "$HOST $HANDLE FAIL did-not-complete"
|
||||||
|
exit 1
|
||||||
Reference in New Issue
Block a user