Add more session tests and small error improvements

This commit is contained in:
Mads Marquart
2020-05-07 10:15:51 +02:00
parent 7be2acad7d
commit 81584d328b
2 changed files with 132 additions and 42 deletions

View File

@@ -4,7 +4,10 @@ import requests
import random import random
import re import re
import json import json
import urllib.parse
# TODO: Only import when required
# Or maybe just replace usage with `html.parser`?
import bs4
from ._common import log, kw_only from ._common import log, kw_only
from . import _graphql, _util, _exception from . import _graphql, _util, _exception
@@ -27,6 +30,10 @@ def parse_server_js_define(html: str) -> Mapping[str, Any]:
rtn = [] rtn = []
if not define_splits: if not define_splits:
raise _exception.ParseError("Could not find any ServerJSDefine", data=html) raise _exception.ParseError("Could not find any ServerJSDefine", data=html)
if len(define_splits) < 2:
raise _exception.ParseError("Could not find enough ServerJSDefine", data=html)
if len(define_splits) > 2:
raise _exception.ParseError("Found too many ServerJSDefine", data=define_splits)
# Parse entries (should be two) # Parse entries (should be two)
for entry in define_splits: for entry in define_splits:
try: try:
@@ -94,16 +101,7 @@ def client_id_factory() -> str:
return hex(int(random.random() * 2 ** 31))[2:] return hex(int(random.random() * 2 ** 31))[2:]
def get_next_url(url: str) -> Optional[str]:
parsed_url = urllib.parse.urlparse(url)
query = urllib.parse.parse_qs(parsed_url.query)
return query.get("next", [None])[0]
def find_form_request(html: str): def find_form_request(html: str):
# Only import when required
import bs4
soup = bs4.BeautifulSoup(html, "html.parser", parse_only=bs4.SoupStrainer("form")) soup = bs4.BeautifulSoup(html, "html.parser", parse_only=bs4.SoupStrainer("form"))
form = soup.form form = soup.form
@@ -114,6 +112,7 @@ def find_form_request(html: str):
if not url: if not url:
raise _exception.ParseError("Could not find url to submit to", data=form) raise _exception.ParseError("Could not find url to submit to", data=form)
# From what I've seen, it'll always do this!
if url.startswith("/"): if url.startswith("/"):
url = "https://www.facebook.com" + url url = "https://www.facebook.com" + url
@@ -168,15 +167,12 @@ def two_factor_helper(session: requests.Session, r, on_2fa_callback):
def get_error_data(html: str) -> Optional[str]: def get_error_data(html: str) -> Optional[str]:
"""Get error message from a request.""" """Get error message from a request."""
# Only import when required
import bs4
soup = bs4.BeautifulSoup( soup = bs4.BeautifulSoup(
html, "html.parser", parse_only=bs4.SoupStrainer("form", id="login_form") html, "html.parser", parse_only=bs4.SoupStrainer("form", id="login_form")
) )
# Attempt to extract and format the error string # Attempt to extract and format the error string
# The error message is in the user's own language! # The error message is in the user's own language!
return ". ".join(list(soup.stripped_strings)[:2]) or None return " ".join(list(soup.stripped_strings)[1:3]) or None
def get_fb_dtsg(define) -> Optional[str]: def get_fb_dtsg(define) -> Optional[str]:
@@ -298,7 +294,7 @@ class Session:
) )
# Get a facebook.com url that handles the 2FA flow # Get a facebook.com url that handles the 2FA flow
# This probably works differently for Messenger-only accounts # This probably works differently for Messenger-only accounts
url = get_next_url(url) url = _util.get_url_parameter(url, "next")
# Explicitly allow redirects # Explicitly allow redirects
r = session.get(url, allow_redirects=True) r = session.get(url, allow_redirects=True)
url = two_factor_helper(session, r, on_2fa_callback) url = two_factor_helper(session, r, on_2fa_callback)

View File

@@ -1,15 +1,47 @@
import datetime import datetime
import pytest import pytest
from fbchat import ParseError
from fbchat._session import ( from fbchat._session import (
parse_server_js_define,
base36encode, base36encode,
prefix_url, prefix_url,
generate_message_id, generate_message_id,
session_factory,
client_id_factory, client_id_factory,
is_home, find_form_request,
get_error_data, get_error_data,
) )
def test_parse_server_js_define():
html = """
some data;require("TimeSliceImpl").guard(function(){(require("ServerJSDefine")).handleDefines([["DTSGInitialData",[],{"token":"123"},100]])
<script>require("TimeSliceImpl").guard(function() {require("ServerJSDefine").handleDefines([["DTSGInitData",[],{"token":"123","async_get_token":"12345"},3333]])
</script>
other irrelevant data
"""
define = parse_server_js_define(html)
assert define == {
"DTSGInitialData": {"token": "123"},
"DTSGInitData": {"async_get_token": "12345", "token": "123"},
}
def test_parse_server_js_define_error():
with pytest.raises(ParseError, match="Could not find any"):
parse_server_js_define("")
html = 'function(){(require("ServerJSDefine")).handleDefines([{"a": function(){}}])'
with pytest.raises(ParseError, match="Invalid"):
parse_server_js_define(html + html)
html = 'function(){require("ServerJSDefine").handleDefines({"a": "b"})'
with pytest.raises(ParseError, match="Invalid"):
parse_server_js_define(html + html)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"number,expected", "number,expected",
[(1, "1"), (10, "a"), (123, "3f"), (1000, "rs"), (123456789, "21i3v9")], [(1, "1"), (10, "a"), (123, "3f"), (1000, "rs"), (123456789, "21i3v9")],
@@ -19,8 +51,10 @@ def test_base36encode(number, expected):
def test_prefix_url(): def test_prefix_url():
assert prefix_url("/") == "https://www.facebook.com/" static_url = "https://upload.messenger.com/"
assert prefix_url("/abc") == "https://www.facebook.com/abc" assert prefix_url(static_url) == static_url
assert prefix_url("/") == "https://www.messenger.com/"
assert prefix_url("/abc") == "https://www.messenger.com/abc"
def test_generate_message_id(): def test_generate_message_id():
@@ -28,46 +62,106 @@ def test_generate_message_id():
assert generate_message_id(datetime.datetime.utcnow(), "def") assert generate_message_id(datetime.datetime.utcnow(), "def")
def test_session_factory():
session = session_factory()
assert session.headers
def test_client_id_factory(): def test_client_id_factory():
# Returns random output, so hard to test more thoroughly # Returns random output, so hard to test more thoroughly
assert client_id_factory() assert client_id_factory()
def test_is_home(): def test_find_form_request():
assert not is_home("https://m.facebook.com/login/?...") html = """
assert is_home("https://m.facebook.com/home.php?refsrc=...") <div>
<form action="/checkpoint/?next=https%3A%2F%2Fwww.messenger.com%2F" class="checkpoint" id="u_0_c" method="post" onsubmit="">
<input autocomplete="off" name="jazoest" type="hidden" value="some-number" />
<input autocomplete="off" name="fb_dtsg" type="hidden" value="some-base64" />
<input class="hidden_elem" data-default-submit="true" name="submit[Continue]" type="submit" />
<input autocomplete="off" name="nh" type="hidden" value="some-hex" />
<div class="_4-u2 _5x_7 _p0k _5x_9 _4-u8">
<div class="_2e9n" id="u_0_d">
<strong id="u_0_e">Two factor authentication required</strong>
<div id="u_0_f"></div>
</div>
<div class="_2ph_">
<input autocomplete="off" name="no_fido" type="hidden" value="true" />
<div class="_50f4">You've asked us to require a 6-digit login code when anyone tries to access your account from a new device or browser.</div>
<div class="_3-8y _50f4">Enter the 6-digit code from your Code Generator or 3rd party app below.</div>
<div class="_2pie _2pio">
<span>
<input aria-label="Login code" autocomplete="off" class="inputtext" id="approvals_code" name="approvals_code" placeholder="Login code" tabindex="1" type="text" />
</span>
</div>
</div>
<div class="_5hzs" id="checkpointBottomBar">
<div class="_2s5p">
<button class="_42ft _4jy0 _2kak _4jy4 _4jy1 selected _51sy" id="checkpointSubmitButton" name="submit[Continue]" type="submit" value="Continue">Continue</button>
</div>
<div class="_2s5q">
<div class="_25b6" id="u_0_g">
<a href="#" id="u_0_h" role="button">Need another way to authenticate?</a>
</div>
</div>
</div>
</div>
</form>
</div>
"""
url, data = find_form_request(html)
def test_find_form_request_error():
with pytest.raises(ParseError, match="Could not find form to submit"):
assert find_form_request("")
with pytest.raises(ParseError, match="Could not find url to submit to"):
assert find_form_request("<form></form>")
@pytest.mark.skip
def test_get_error_data(): def test_get_error_data():
html = """<?xml version="1.0" encoding="utf-8"?> html = """<!DOCTYPE html>
<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.0//EN" "http://www.wapforum.org/DTD/xhtml-mobile10.dtd"> <html lang="da" id="facebook" class="no_js">
<html xmlns="http://www.w3.org/1999/xhtml">
<head> <head>
<title>Log in to Facebook | Facebook</title> <meta charset="utf-8" />
<meta name="referrer" content="origin-when-crossorigin" id="meta_referrer" /> <title id="pageTitle">Messenger</title>
<style type="text/css">...</style> <meta name="referrer" content="default" id="meta_referrer" />
<meta name="description" content="..." />
<link rel="canonical" href="https://www.facebook.com/login/" />
</head> </head>
<body tabindex="0" class="b c d e f g"> <body class="_605a x1 Locale_da_DK" dir="ltr">
<div class="h"><div id="viewport">...<div id="objects_container"><div class="g" id="root" role="main"> <div class="_3v_o" id="XMessengerDotComLoginViewPlaceholder">
<table class="x" role="presentation"><tbody><tr><td class="y"> <form id="login_form" action="/login/password/" method="post" onsubmit="">
<div class="z ba bb" style="" id="login_error"> <input type="hidden" name="jazoest" value="2222" autocomplete="off" />
<div class="bc"> <input type="hidden" name="lsd" value="xyz-abc" autocomplete="off" />
<span>The password you entered is incorrect. <a href="/recover/initiate/?ars=facebook_login_pw_error&amp;email=abc@mail.com&amp;__ccr=XXX" class="bd" aria-label="Have you forgotten your password?">Did you forget your password?</a></span> <div class="_3403 _3404">
<div>Type your password again</div>
<div>The password you entered is incorrect. <a href="https://www.facebook.com/recover/initiate?ars=facebook_login_pw_error">Did you forget your password?</a></div>
</div> </div>
<div id="loginform">
<input type="hidden" autocomplete="off" id="initial_request_id" name="initial_request_id" value="xxx" />
<input type="hidden" autocomplete="off" name="timezone" value="" id="u_0_1" />
<input type="hidden" autocomplete="off" name="lgndim" value="" id="u_0_2" />
<input type="hidden" name="lgnrnd" value="aaa" />
<input type="hidden" id="lgnjs" name="lgnjs" value="n" />
<input type="text" class="inputtext _55r1 _43di" id="email" name="email" placeholder="E-mail or phone number" value="some@email.com" tabindex="0" aria-label="E-mail or phone number" />
<input type="password" class="inputtext _55r1 _43di" name="pass" id="pass" tabindex="0" placeholder="Password" aria-label="Password" />
<button value="1" class="_42ft _4jy0 _2m_r _43dh _4jy4 _517h _51sy" id="loginbutton" name="login" tabindex="0" type="submit">Continue</button>
<div class="_43dj">
<div class="uiInputLabel clearfix">
<label class="uiInputLabelInput">
<input type="checkbox" value="1" name="persistent" tabindex="0" class="" id="u_0_0" />
<span class=""></span>
</label>
<label for="u_0_0" class="uiInputLabelLabel">Stay logged in</label>
</div>
<input type="hidden" autocomplete="off" id="default_persistent" name="default_persistent" value="0" />
</div>
</form>
</div> </div>
...
</td></tr></tbody></table>
<div style="display:none"></div><span><img src="https://facebook.com/security/hsts-pixel.gif" width="0" height="0" style="display:none" /></span>
</div></div><div></div></div></div>
</body> </body>
</html> </html>
""" """
url = "https://m.facebook.com/login/?email=abc@mail.com&li=XXX&e=1348092"
msg = "The password you entered is incorrect. Did you forget your password?" msg = "The password you entered is incorrect. Did you forget your password?"
assert (1348092, msg) == get_error_data(html) assert msg == get_error_data(html)