7.4 KiB
Camoufox Registration Patterns (2026-08-10)
OctoCaptcha Form Validation (_wait_form_valid)
GitHub signup has two independent gates keeping the submit button disabled:
- Field auto-checks (email validity, username, password)
- OctoCaptcha (DataDome iframe) — hidden
octocaptcha-tokenstays empty until verification finishes
Must wait for BOTH before clicking submit. JS evaluation to probe state:
def wait_form_valid(page, timeout_s=60):
"""Wait for GitHub's async field validation AND octocaptcha to resolve."""
deadline = time.time() + timeout_s
while time.time() < deadline:
state = page.evaluate("""() => {
const out = {email_taken: false, errors: [], pending: false,
fields_valid: false, captcha_pending: false,
submit_enabled: false};
const fields = [
['email', 'input[name="user[email]"], #email'],
['username', 'input[name="user[login]"], #login'],
['password', 'input[name="user[password]"], #password'],
];
let successCount = 0, total = 0;
for (const [label, sel] of fields) {
const el = document.querySelector(sel);
if (!el) continue;
total += 1;
if (el.getAttribute('aria-invalid') === 'true') {
const container = el.closest('auto-check') || el.parentElement;
const errEl = container ? container.querySelector('.error') : null;
const txt = errEl ? errEl.textContent.trim().replace(/\\s+/g,' ') : (label + ' invalid');
out.errors.push(label + ': ' + txt.slice(0, 160));
if (/already associated|already been taken|already taken/i.test(txt))
out.email_taken = true;
} else if (el.classList.contains('is-autocheck-successful')) {
successCount += 1;
}
if (el.classList.contains('is-autocheck-in-progress')) out.pending = true;
}
out.fields_valid = (total > 0 && successCount === total && out.errors.length === 0);
const tok = document.querySelector('input[name="octocaptcha-token"], .js-octocaptcha-token');
const tokenVal = tok ? (tok.value || '').trim() : '';
const spinner = document.querySelector('.js-octocaptcha-spinner');
const success = document.querySelector('.js-octocaptcha-success');
out.captcha_pending = (!!tok && !tokenVal && !success.classList.contains('d-none') && !!spinner && !spinner.classList.contains('d-none'));
const email = document.querySelector('input[name="user[email]"], #email');
const form = email ? email.closest('form') : null;
if (form) {
const b = Array.from(form.querySelectorAll('button, input[type=submit]'))
.find(x => (x.getAttribute('name')==='commit') ||
/create account/i.test(x.textContent||x.value||''));
if (b) out.submit_enabled = !b.disabled;
}
return out;
}""")
if state.get("submit_enabled") and not state.get("pending"):
return {"ready": True}
time.sleep(1)
return {"ready": False, "timeout": True}
Proper Click (NOT social buttons)
GitHub's signup page has BOTH email-signup submit buttons AND social-login buttons ("Continue with Google/Apple"). A naive page.click("button:has-text('Create account')") may click the wrong button.
Use JS evaluation to find the right button:
page.evaluate("""() => {
const email = document.querySelector('input[name="user[email]"], #email');
const form = email.closest('form');
const act = (form.getAttribute('action') || '').toLowerCase();
// Skip social/OAuth forms
if (act.includes('/sessions/social/') || act.includes('oauth')) return 'social_form';
const btns = Array.from(form.querySelectorAll('button, input[type=submit]'));
const good = btns.find(b => {
const t = (b.textContent || b.value || '').toLowerCase();
const prov = b.getAttribute('data-provider') || '';
if (prov) return false; // skip social buttons
return t.includes('create') || t.includes('account') || b.getAttribute('name') === 'commit';
});
if (good) { good.click(); return 'clicked'; }
return 'no_button';
}""")
Navigation Wait (post-submit)
After clicking submit, the page navigates from /signup to /account_verifications. The navigation can be slow via proxies (30+ seconds). Instead of polling page.url (which is a live property — see pitfall), use page.wait_for_selector:
try:
page.wait_for_selector('input[name="launch_code[]"]', state="visible", timeout=90000)
# On verification page
except:
# Check for rate limit or unexpected redirect
body = page.inner_text("body")[:500]
if "rate limit" in body.lower():
return "rate_limited"
Camoufox Launch Options
launch_opts = {"headless": False, "geoip": True}
if proxy_config:
launch_opts["proxy"] = {
"server": proxy["server"],
"username": proxy["username"],
"password": proxy["password"],
}
with Camoufox(**launch_opts) as browser:
...
geoip=True is recommended when using proxies to avoid LeakWarning.
Mail.tm API
base = "https://api.mail.tm"
# Create account
r = requests.post(f"{base}/accounts", json={"address": f"{username}@{domain}", "password": pw})
token = [REDACTED]"{base}/token", json={"address": email, "password": pw}).json()["token"]
# Poll inbox
headers = {"Authorization": f"Bearer {token}"}
r = requests.get(f"{base}/messages", headers=headers)
Pitfalls from this session
page.url is a LIVE property
page.url re-evaluates every time it's accessed. Between log(f"URL: {page.url}") and if "account_verif" in page.url:, the page can navigate.
Fix: read once into a local variable.
Post-OTP "accepted" without session
After OTP, GitHub may redirect to root path (/ or /dashboard) making it look like success, but the account actually needs a post-signup login.
Fix: after "accepted" result, navigate to /settings/profile. If redirected to /login, do the post-signup login.
Rate limit persistence
GitHub secondary rate limits can persist for hours, not minutes. A 15-minute wait is often insufficient. Rotate IPs aggressively.
Proxy pool dead (2026-08-10)
bpproxy.at residential pool — ALL 10 proxies dead. curl returns empty response, Camoufox returns NS_ERROR_PROXY_CONNECTION_REFUSED. Proxies are not rate-limited — they are physically unreachable. Always verify proxy alive before use:
curl -sI --max-time 10 -x <proxy_url> https://github.com | head -1
If no HTTP/1.1 200 response, proxy is dead — don't use it.
Camoufox proxy config format
Playwright REQUIRES credentials in separate username/password keys:
from core.proxy_utils import playwright_proxy_config
# Input: "http://user:pass@host:1000"
# Output: {"server": "http://host:1000", "username": "user", "password": "pass"}
Embedding credentials in the server field yields NS_ERROR_PROXY_CONNECTION_REFUSED.
mail.tm domains blocked (2026-08-10)
GitHub silently rejects accounts registered with mail.tm domains (emalupe.com). OTP is accepted, but account returns 404 on API check. Use t-online.de IMAP pool instead.