Skip to content

Commit

Permalink
feat: IMAP Integration for 2FA [BREAKING] (#104)
Browse files Browse the repository at this point in the history
* IMAP Integration

* IMAP Integration

* IMAP Integration

* IMAP Integration

* IMAP Integration

* Minor changes to match current master branch

* Updated to latest build

* Fixed quoted out variable

* Changed case from lower to cammelCase

* Update to accomidate for case change

* Add imaplib2

* Resolved verion issue

* Delete Logger.py

* Resolve version issue

* Added check to circumvent late IMAP logins

Intended to prevent the event in which the IMAP session is created after the email is received from a riot by adding a timeout. Also added more exceptions + integrated it with all new commits to `master`.

* New exceptions for IMAP

* Remove print statements

* Fixed issues, as per request of Poro

* Fixed API "crashing" + removed print statements

see PR [#154](#154)

* Fixed case issue

* Refresh fix + IMAP timeout fix

* Delete InvalidIMAPCredentials.py

* Add files via upload

* Line Endings

* Fixed line endings

* Remove print statement

* Lock the UI updates

---------

Co-authored-by: League of Poro <95635582+LeagueOfPoro@users.noreply.github.com>
  • Loading branch information
Skribb11es and LeagueOfPoro authored Feb 22, 2023
1 parent 83208a7 commit 5a3f700
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 76 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ beautifulsoup4 = "*"
pyyaml = "*"
rich = "*"
pyjwt = "*"
imaplib2 = "*"

[dev-packages]
autopep8 = "*"
Expand Down
72 changes: 59 additions & 13 deletions config/confighelper.html

Large diffs are not rendered by default.

99 changes: 70 additions & 29 deletions src/Browser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from AssertCondition import AssertCondition
from Exceptions.NoAccessTokenException import NoAccessTokenException
from Exceptions.RateLimitException import RateLimitException
from Exceptions.InvalidIMAPCredentialsException import InvalidIMAPCredentialsException
from Exceptions.Fail2FAException import Fail2FAException
from Exceptions.FailFind2FAException import FailFind2FAException
from Match import Match
import cloudscraper
from pprint import pprint
Expand All @@ -13,6 +16,8 @@
import pickle
from pathlib import Path
import jwt
from IMAP import IMAP # Added to automate 2FA
import imaplib2

from SharedData import SharedData

Expand All @@ -21,7 +26,7 @@ class Browser:
SESSION_REFRESH_INTERVAL = 1800.0
STREAM_WATCH_INTERVAL = 60.0

def __init__(self, log, config: Config, account: str, sharedData: SharedData):
def __init__(self, log, stats, config: Config, account: str, sharedData: SharedData):
"""
Initialize the Browser class
Expand All @@ -36,13 +41,16 @@ def __init__(self, log, config: Config, account: str, sharedData: SharedData):
'desktop': True
},
debug=False)

self.log = log
self.stats = stats
self.config = config
self.currentlyWatching = {}
self.account = account
self.sharedData = sharedData
self.ref = "Referer"

def login(self, username: str, password: str, refreshLock) -> bool:
def login(self, username: str, password: str, imapusername: str, imappassword: str, imapserver: str, refreshLock) -> bool:
"""
Login to the website using given credentials. Obtain necessary tokens.
Expand All @@ -64,15 +72,31 @@ def login(self, username: str, password: str, refreshLock) -> bool:
if res.status_code == 429:
retryAfter = res.headers['Retry-after']
raise RateLimitException(retryAfter)

resJson = res.json()
if "multifactor" in resJson.get("type", ""):
twoFactorCode = input(f"Enter 2FA code for {self.account}:\n")
print("Code sent")
data = {"type": "multifactor", "code": twoFactorCode, "rememberDevice": True}
res = self.client.put(
"https://auth.riotgames.com/api/v1/authorization", json=data)
resJson = res.json()
refreshLock.release()
if (imapserver != ""):
#Handles all IMAP requests
req = self.IMAPHook(imapusername, imappassword, imapserver)

self.stats.updateStatus(self.account, f"[green]FETCHED 2FA CODE")

data = {"type": "multifactor", "code": req.code, "rememberDevice": True}
res = self.client.put(
"https://auth.riotgames.com/api/v1/authorization", json=data)
resJson = res.json()
if 'error' in resJson:
if resJson['error'] == 'multifactor_attempt_failed':
raise Fail2FAException

else:
twoFactorCode = input(f"Enter 2FA code for {self.account}:\n")
self.stats.updateStatus(self.account, f"[green]CODE SENT")
data = {"type": "multifactor", "code": twoFactorCode, "rememberDevice": True}
res = self.client.put(
"https://auth.riotgames.com/api/v1/authorization", json=data)
resJson = res.json()
# Finish OAuth2 login
res = self.client.get(resJson["response"]["parameters"]["uri"])
except KeyError:
Expand All @@ -81,7 +105,8 @@ def login(self, username: str, password: str, refreshLock) -> bool:
self.log.error(f"You are being rate-limited. Retry after {ex}")
return False
finally:
refreshLock.release()
if refreshLock.locked():
refreshLock.release()
# Login to lolesports.com, riotgames.com, and playvalorant.com
token, state = self.__getLoginTokens(res.text)
if token and state:
Expand All @@ -97,30 +122,50 @@ def login(self, username: str, password: str, refreshLock) -> bool:
self.client.get(
"https://auth.riotgames.com/authorize?client_id=esports-rna-prod&redirect_uri=https://account.rewards.lolesports.com/v1/session/oauth-callback&response_type=code&scope=openid&prompt=none&state=https://lolesports.com/?memento=na.en_GB", allow_redirects=True).close()

# Get access and entitlement tokens for the first time
headers = {"Origin": "https://lolesports.com",
"Referrer": "https://lolesports.com"}
def reqAcc():
# This requests sometimes returns 404
return self.client.get(
"https://account.rewards.lolesports.com/v1/session/token", headers={"Origin": "https://lolesports.com", self.ref: "https://lolesports.com"})


resAccessToken = reqAcc()

if resAccessToken.status_code != 200 and self.ref == "Referer":
self.ref = "Referrer"
reqAcc()
elif resAccessToken.status_code != 200 and self.ref == "Referrer":
self.ref = "Referer"
reqAcc()

# This requests sometimes returns 404
resAccessToken = self.client.get(
"https://account.rewards.lolesports.com/v1/session/token", headers=headers)
# Currently unused but the call might be important server-side
resPasToken = self.client.get(
"https://account.rewards.lolesports.com/v1/session/clientconfig/rms", headers=headers).close()
"https://account.rewards.lolesports.com/v1/session/clientconfig/rms", headers={"Origin": "https://lolesports.com", self.ref: "https://lolesports.com"}).close()
if resAccessToken.status_code == 200:
self.__dumpCookies()
return True
return False

def IMAPHook(self, usern, passw, server):
try:
M = imaplib2.IMAP4_SSL(server)
M.login(usern, passw)
M.select("INBOX")
idler = IMAP(M)
idler.start()
idler.join()
M.logout()
return idler
except FailFind2FAException:
self.log.error(f"Failed to find 2FA code for {self.account}")
except:
raise InvalidIMAPCredentialsException()

def refreshSession(self):
"""
Refresh access and entitlement tokens
"""
try:
headers = {"Origin": "https://lolesports.com",
"Referrer": "https://lolesports.com"}
resAccessToken = self.client.get(
"https://account.rewards.lolesports.com/v1/session/refresh", headers=headers)
"https://account.rewards.lolesports.com/v1/session/refresh")
AssertCondition.statusCodeMatches(200, resAccessToken)
resAccessToken.close()
self.__dumpCookies()
Expand Down Expand Up @@ -153,10 +198,7 @@ def sendWatchToLive(self) -> list:

def checkNewDrops(self, lastCheckTime):
try:
headers = {"Origin": "https://lolesports.com",
"Referrer": "https://lolesports.com",
"Authorization": "Cookie access_token"}
res = self.client.get("https://account.service.lolesports.com/fandom-account/v1/earnedDrops?locale=en_GB&site=LOLESPORTS", headers=headers)
res = self.client.get("https://account.service.lolesports.com/fandom-account/v1/earnedDrops?locale=en_GB&site=LOLESPORTS", headers={"Origin": "https://lolesports.com", "Authorization": "Cookie access_token"})
resJson = res.json()
res.close()
return [drop for drop in resJson if lastCheckTime <= drop["unlockedDateMillis"]]
Expand All @@ -170,7 +212,7 @@ def __needSessionRefresh(self) -> bool:

res = jwt.decode(self.client.cookies.get_dict()["access_token"], options={"verify_signature": False})
timeLeft = res['exp'] - int(time())
self.log.debug(f"{timeLeft} s until session expires.")
self.log.debug(f"{timeLeft}s until session expires.")
if timeLeft < 600:
return True
return False
Expand All @@ -187,10 +229,9 @@ def __sendWatch(self, match: Match):
"stream_position_time": datetime.utcnow().isoformat(sep='T', timespec='milliseconds')+'Z',
"geolocation": {"code": "CZ", "area": "EU"},
"tournament_id": match.tournamentId}
headers = {"Origin": "https://lolesports.com",
"Referrer": "https://lolesports.com"}

res = self.client.post(
"https://rex.rewards.lolesports.com/v1/events/watch", headers=headers, json=data)
"https://rex.rewards.lolesports.com/v1/events/watch", json=data)
AssertCondition.statusCodeMatches(201, res)
res.close()

Expand Down
29 changes: 17 additions & 12 deletions src/Config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import requests
import yaml
import yaml, requests
from yaml.parser import ParserError
from rich import print
from pathlib import Path
Expand All @@ -20,20 +19,28 @@ def __init__(self, configPath: str) -> None:
:param configPath: string, path to the configuration file
"""

self.accounts = {}
try:
configPath = self.__findConfig(configPath)
with open(configPath, "r", encoding='utf-8') as f:
config = yaml.safe_load(f)
accs = config.get("accounts")
onlyDefaultUsername = True
for account in accs:
self.accounts[account] = {
#Orig data
"username": accs[account]["username"],
"password": accs[account]["password"],

#IMAP data
"imapUsername": accs[account].get("imapUsername", ""),
"imapPassword": accs[account].get("imapPassword", ""),
"imapServer": accs[account].get("imapServer", ""),
}
if "username" != accs[account]["username"]:
self.accounts[account] = {
"username": accs[account]["username"],
"password": accs[account]["password"]
}
if not self.accounts:
onlyDefaultUsername = False
if onlyDefaultUsername:
raise InvalidCredentialsException
self.debug = config.get("debug", False)
self.connectorDrops = config.get("connectorDropsUrl", "")
Expand All @@ -52,8 +59,6 @@ def __init__(self, configPath: str) -> None:
print("Press any key to exit...")
input()
raise ex

# Get bestStreams from URL
try:
remoteBestStreamsFile = requests.get(self.REMOTE_BEST_STREAMS_URL)
if remoteBestStreamsFile.status_code == 200:
Expand All @@ -64,7 +69,6 @@ def __init__(self, configPath: str) -> None:
input()
raise ex


def getAccount(self, account: str) -> dict:
"""
Get account information
Expand All @@ -73,7 +77,7 @@ def getAccount(self, account: str) -> dict:
:return: dictionary, account information
"""
return self.accounts[account]

def __findConfig(self, configPath):
"""
Try to find configuartion file in alternative locations.
Expand All @@ -88,4 +92,5 @@ def __findConfig(self, configPath):
return Path("../config/config.yaml")
if Path("config/config.yaml").exists():
return Path("config/config.yaml")

return configPath
6 changes: 2 additions & 4 deletions src/DataProviderThread.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ def fetchLiveMatches(self):
"""
Retrieve data about currently live matches and store them.
"""
headers = {"Origin": "https://lolesports.com", "Referrer": "https://lolesports.com",
"x-api-key": "0TvQnueqKa5mxJntVWt0w4LpLfEkrV1Ta8rQBb9Z"}
headers = {"Origin": "https://lolesports.com", "x-api-key": "0TvQnueqKa5mxJntVWt0w4LpLfEkrV1Ta8rQBb9Z"}
res = self.client.get(
"https://esports-api.lolesports.com/persisted/gw/getLive?hl=en-GB", headers=headers)
AssertCondition.statusCodeMatches(200, res)
Expand Down Expand Up @@ -75,8 +74,7 @@ def fetchTimeUntilNextMatch(self):
"""
Retrieve data about currently live matches and store them.
"""
headers = {"Origin": "https://lolesports.com", "Referrer": "https://lolesports.com",
"x-api-key": "0TvQnueqKa5mxJntVWt0w4LpLfEkrV1Ta8rQBb9Z"}
headers = {"Origin": "https://lolesports.com", "x-api-key": "0TvQnueqKa5mxJntVWt0w4LpLfEkrV1Ta8rQBb9Z"}
try:
res = self.client.get(
"https://esports-api.lolesports.com/persisted/gw/getSchedule?hl=en-GB", headers=headers)
Expand Down
5 changes: 5 additions & 0 deletions src/Exceptions/Fail2FAException.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from Exceptions.CapsuleFarmerEvolvedException import CapsuleFarmerEvolvedException

class Fail2FAException(CapsuleFarmerEvolvedException):
def __init__(self):
super().__init__("Unable to login with 2FA.")
5 changes: 5 additions & 0 deletions src/Exceptions/FailFind2FAException.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from Exceptions.CapsuleFarmerEvolvedException import CapsuleFarmerEvolvedException

class FailFind2FAException(CapsuleFarmerEvolvedException):
def __init__(self):
super().__init__("Failed to find 2FA email.")
5 changes: 5 additions & 0 deletions src/Exceptions/InvalidIMAPCredentialsException.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from Exceptions.CapsuleFarmerEvolvedException import CapsuleFarmerEvolvedException

class InvalidIMAPCredentialsException(CapsuleFarmerEvolvedException):
def __init__(self):
super().__init__("Invalid IMAP credentials.")
12 changes: 9 additions & 3 deletions src/FarmThread.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from threading import Thread
from time import sleep
from Browser import Browser
from Exceptions.InvalidIMAPCredentialsException import InvalidIMAPCredentialsException
from Exceptions.Fail2FAException import Fail2FAException
import requests

from SharedData import SharedData
Expand All @@ -25,7 +27,7 @@ def __init__(self, log, config, account, stats, locks, sharedData: SharedData):
self.config = config
self.account = account
self.stats = stats
self.browser = Browser(self.log, self.config, self.account, sharedData)
self.browser = Browser(self.log, self.stats, self.config, self.account, sharedData)
self.locks = locks
self.sharedData = sharedData

Expand All @@ -35,7 +37,7 @@ def run(self):
"""
try:
self.stats.updateStatus(self.account, "[yellow]LOGIN")
if self.browser.login(self.config.getAccount(self.account)["username"], self.config.getAccount(self.account)["password"], self.locks["refreshLock"]):
if self.browser.login(self.config.getAccount(self.account)["username"], self.config.getAccount(self.account)["password"], self.config.getAccount(self.account)["imapUsername"], self.config.getAccount(self.account)["imapPassword"], self.config.getAccount(self.account)["imapServer"], self.locks["refreshLock"]):
self.stats.updateStatus(self.account, "[green]LIVE")
self.stats.resetLoginFailed(self.account)
while True:
Expand Down Expand Up @@ -73,6 +75,10 @@ def run(self):
self.stats.updateStatus(self.account, "[red]LOGIN FAILED - WILL RETRY SOON")
else:
self.stats.updateStatus(self.account, "[red]LOGIN FAILED")
except InvalidIMAPCredentialsException:
self.log.error(f"IMAP login failed for {self.account}")
self.stats.updateStatus(self.account, "[red]IMAP LOGIN FAILED")
self.stats.updateThreadStatus(self.account)
except Exception:
self.log.exception(f"Error in {self.account}. The program will try to recover.")

Expand Down Expand Up @@ -120,4 +126,4 @@ def getLeagues():
"https://esports-api.lolesports.com/persisted/gw/getLeagues?hl=en-GB", headers=headers)
leagues = res.json()["data"].get("leagues", [])
res.close()
return leagues
return leagues
3 changes: 2 additions & 1 deletion src/GuiThread.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def run(self):
sleep(1)
self.locks["refreshLock"].acquire()
live.refresh()
self.locks["refreshLock"].release()
if self.locks["refreshLock"].locked():
self.locks["refreshLock"].release()

def stop(self):
"""
Expand Down
Loading

0 comments on commit 5a3f700

Please sign in to comment.