mirror of
https://github.com/JimmXinu/FanFicFare.git
synced 2025-12-06 08:52:55 +01:00
Implementing Timed One Time Password(TOTP) 2FA Exception and collection
This commit is contained in:
parent
c3a90a8914
commit
6c0df42fe7
10 changed files with 128 additions and 12 deletions
|
|
@ -620,6 +620,48 @@ class UserPassDialog(QDialog):
|
|||
self.status=False
|
||||
self.hide()
|
||||
|
||||
class TOTPDialog(QDialog):
|
||||
'''
|
||||
Need to collect Timebased One Time Password(TOTP) for some sites.
|
||||
'''
|
||||
def __init__(self, gui, site, exception=None):
|
||||
QDialog.__init__(self, gui)
|
||||
self.status=False
|
||||
|
||||
self.l = QVBoxLayout()
|
||||
self.setLayout(self.l)
|
||||
|
||||
grid = QGridLayout()
|
||||
self.l.addLayout(grid)
|
||||
|
||||
self.setWindowTitle(_('Time-based One Time Password(TOTP)'))
|
||||
grid.addWidget(QLabel(_("Site requires a Time-based One Time Password(TOTP) for this url:\n%s")%exception.url),0,0,1,2)
|
||||
|
||||
grid.addWidget(QLabel(_("TOTP:")),2,0)
|
||||
self.totp = QLineEdit(self)
|
||||
grid.addWidget(self.totp,2,1)
|
||||
|
||||
horz = QHBoxLayout()
|
||||
self.l.addLayout(horz)
|
||||
|
||||
self.ok_button = QPushButton(_('OK'), self)
|
||||
self.ok_button.clicked.connect(self.ok)
|
||||
horz.addWidget(self.ok_button)
|
||||
|
||||
self.cancel_button = QPushButton(_('Cancel'), self)
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
horz.addWidget(self.cancel_button)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def ok(self):
|
||||
self.status=True
|
||||
self.hide()
|
||||
|
||||
def cancel(self):
|
||||
self.status=False
|
||||
self.hide()
|
||||
|
||||
def LoopProgressDialog(gui,
|
||||
book_list,
|
||||
foreach_function,
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ from calibre_plugins.fanficfare_plugin.prefs import (
|
|||
from calibre_plugins.fanficfare_plugin.dialogs import (
|
||||
AddNewDialog, UpdateExistingDialog,
|
||||
LoopProgressDialog, UserPassDialog, AboutDialog, CollectURLDialog,
|
||||
RejectListDialog, EmailPassDialog,
|
||||
RejectListDialog, EmailPassDialog, TOTPDialog,
|
||||
save_collisions, question_dialog_all,
|
||||
NotGoingToDownload, RejectUrlEntry, IniTextDialog)
|
||||
|
||||
|
|
@ -1240,9 +1240,9 @@ class FanFicFarePlugin(InterfaceAction):
|
|||
|
||||
def get_story_metadata_only(self,adapter):
|
||||
url = adapter.url
|
||||
## three tries, that's enough if both user/pass & is_adult needed,
|
||||
## or a couple tries of one or the other
|
||||
for x in [0,1,2]:
|
||||
## 5 tries, should be enough if user/pass, totp & is_adult
|
||||
## needed, or a couple tries of one or the other
|
||||
for x in [0,1,2,3,4]:
|
||||
try:
|
||||
adapter.getStoryMetadataOnly(get_cover=False)
|
||||
except exceptions.FailedToLogin as f:
|
||||
|
|
@ -1253,6 +1253,13 @@ class FanFicFarePlugin(InterfaceAction):
|
|||
adapter.username = userpass.user.text()
|
||||
adapter.password = userpass.passwd.text()
|
||||
|
||||
except exceptions.NeedTimedOneTimePassword as e:
|
||||
logger.warn("Login Failed, Need Username/Password.")
|
||||
totpdlg = TOTPDialog(self.gui,url,e)
|
||||
totpdlg.exec_() # exec_ will make it act modal
|
||||
if totpdlg.status:
|
||||
adapter.totp = totpdlg.totp.text()
|
||||
|
||||
except exceptions.AdultCheckRequired:
|
||||
if question_dialog_all(self.gui, _('Are You an Adult?'), '<p>'+
|
||||
_("%s requires that you be an adult. Please confirm you are an adult in your locale:")%url,
|
||||
|
|
@ -1428,6 +1435,7 @@ class FanFicFarePlugin(InterfaceAction):
|
|||
book['is_adult'] = adapter.is_adult
|
||||
book['username'] = adapter.username
|
||||
book['password'] = adapter.password
|
||||
book['totp'] = adapter.totp
|
||||
|
||||
book['icon'] = 'plus.png'
|
||||
book['status'] = _('Add')
|
||||
|
|
@ -3110,7 +3118,7 @@ def pretty_book(d, indent=0, spacer=' '):
|
|||
# return '\n'.join([(pretty_book(v, indent, spacer)) for v in d])
|
||||
|
||||
if isinstance(d, dict):
|
||||
for k in ('password','username'):
|
||||
for k in ('password','username','totp'):
|
||||
if k in d and d[k]:
|
||||
d[k]=_('(was set, removed for security)')
|
||||
return '\n'.join(['%s%s:\n%s' % (kindent, k, pretty_book(v, indent + 1, spacer))
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ def do_download_for_worker(book,options,merge,notification=lambda x,y:x):
|
|||
adapter.is_adult = book['is_adult']
|
||||
adapter.username = book['username']
|
||||
adapter.password = book['password']
|
||||
adapter.totp = book['totp']
|
||||
adapter.setChaptersRange(book['begin'],book['end'])
|
||||
|
||||
## each site download job starts with a new copy of the
|
||||
|
|
|
|||
|
|
@ -147,6 +147,21 @@ class StoriesOnlineNetAdapter(BaseSiteAdapter):
|
|||
postAction,
|
||||
'','',''))
|
||||
data = self.post_request(postUrl,params,usecache=False)
|
||||
# logger.debug(data)
|
||||
while '<h2>Enter TOTP Code:</h2>' in data:
|
||||
if self.totp:
|
||||
logger.debug("Trying to TOTP with %s code."%self.totp)
|
||||
params = {}
|
||||
params['cmd'] = 'finishTotpVerification'
|
||||
# google auth app at least shows "123 123", but site expects
|
||||
# "123123". Remove space if user enters it.
|
||||
params['totp_code'] = self.totp.replace(' ','')
|
||||
params['action'] = "continue"
|
||||
data = self.post_request(postUrl,params,usecache=False)
|
||||
# logger.debug(data)
|
||||
self.totp = None
|
||||
else:
|
||||
raise exceptions.NeedTimedOneTimePassword(url)
|
||||
|
||||
if self.needToLoginCheck(data):
|
||||
logger.info("Failed to login to URL %s as %s" % (loginUrl,
|
||||
|
|
|
|||
|
|
@ -129,6 +129,9 @@ Some more longer description. "I suck at summaries!" "Better than it sounds!"
|
|||
else:
|
||||
self.story.setMetadata('dateUpdated',makeDate("1975-04-15","%Y-%m-%d"))
|
||||
|
||||
if idstr == '675' and self.totp != "123321" :
|
||||
raise exceptions.NeedTimedOneTimePassword(self.url)
|
||||
|
||||
if idstr != '674':
|
||||
self.story.setMetadata('numWords','123456')
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ class TheSietchComAdapter(BaseXenForo2ForumAdapter):
|
|||
# in case it needs more than just site/
|
||||
return '/index.php?'
|
||||
|
||||
def loginFormMarker(self):
|
||||
return 'href="/index.php?login/"'
|
||||
|
||||
def make_reader_url(self,tmcat_num,reader_page_num):
|
||||
# https://www.the-sietch.com/index.php?threads/shattered-sphere-the-arcadian-free-march.3243/reader/page-2
|
||||
# discard tmcat_num -- the-sietch.com doesn't have multiple
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class BaseSiteAdapter(Requestable):
|
|||
|
||||
self.username = "NoneGiven" # if left empty, site doesn't return any message at all.
|
||||
self.password = ""
|
||||
self.totp = None # Timed One Time Password(TOTP) for 2FA
|
||||
self.is_adult=False
|
||||
|
||||
self.storyDone = False
|
||||
|
|
|
|||
|
|
@ -41,10 +41,14 @@ class BaseXenForo2ForumAdapter(BaseXenForoForumAdapter):
|
|||
"Only needs to be overriden if has additional ini sections."
|
||||
return super(BaseXenForo2ForumAdapter, cls).getConfigSections() + ['base_xenforo2forum']
|
||||
|
||||
## the-sietch.com needs a different value.
|
||||
def loginFormMarker(self):
|
||||
return 'href="/login/"'
|
||||
|
||||
def performLogin(self,data):
|
||||
params = {}
|
||||
|
||||
if data and 'href="/login/"' not in data:
|
||||
if data and self.loginFormMarker() not in data:
|
||||
## already logged in.
|
||||
logger.debug("Already Logged In")
|
||||
return
|
||||
|
|
@ -75,16 +79,40 @@ class BaseXenForo2ForumAdapter(BaseXenForoForumAdapter):
|
|||
logger.debug("Will now login to URL (%s) as (%s)" % (loginUrl,
|
||||
params['login']))
|
||||
|
||||
d = self.post_request(loginUrl, params)
|
||||
data = self.post_request(loginUrl, params)
|
||||
# logger.debug(data)
|
||||
|
||||
if "Log in" in d:
|
||||
# logger.debug(d)
|
||||
while "Please enter the verification code generated by the app on your phone" in data:
|
||||
logger.info("TOTP required to login to URL %s" % self.url)
|
||||
if self.totp:
|
||||
logger.debug("Trying to TOTP with %s code."%self.totp)
|
||||
postUrl = self.getURLPrefix() + 'login/two-step'
|
||||
totpparams = {}
|
||||
xftoken = data[data.index(find_token)+len(find_token):]
|
||||
xftoken = xftoken[:xftoken.index('"')]
|
||||
totpparams['remember'] = '0'
|
||||
totpparams['confirm'] = '1'
|
||||
totpparams['provider'] = 'totp'
|
||||
totpparams['_xfResponseType'] = 'json'
|
||||
totpparams['_xfToken'] = xftoken
|
||||
totpparams['_xfRedirect'] = self.getURLPrefix()
|
||||
totpparams['_xfWithData'] = '1'
|
||||
# google auth app at least shows "123 123", but site expects
|
||||
# "123123". Remove space if user enters it.
|
||||
totpparams['code'] = self.totp.replace(' ','')
|
||||
data = self.post_request(postUrl,totpparams,usecache=False)
|
||||
# logger.debug(data)
|
||||
self.totp = None
|
||||
else:
|
||||
raise exceptions.NeedTimedOneTimePassword(self.url)
|
||||
return False
|
||||
if "Log in" in data:
|
||||
# logger.debug(data)
|
||||
logger.info("Failed to login to URL %s as %s" % (self.url,
|
||||
params['login']))
|
||||
raise exceptions.FailedToLogin(self.url,params['login'])
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return True
|
||||
|
||||
def parse_title(self,souptag):
|
||||
h1 = souptag.find('h1',{'class':'p-title-value'})
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ def do_download(arg,
|
|||
|
||||
# three tries, that's enough if both user/pass & is_adult needed,
|
||||
# or a couple tries of one or the other
|
||||
for x in range(0, 2):
|
||||
for x in range(0, 4):
|
||||
try:
|
||||
adapter.getStoryMetadataOnly()
|
||||
except exceptions.FailedToLogin as f:
|
||||
|
|
@ -447,6 +447,14 @@ def do_download(arg,
|
|||
adapter.username = sys.stdin.readline().strip()
|
||||
adapter.password = getpass.getpass(prompt='Password: ')
|
||||
# print('Login: `%s`, Password: `%s`' % (adapter.username, adapter.password))
|
||||
except exceptions.NeedTimedOneTimePassword as e:
|
||||
if not options.interactive:
|
||||
print('Login Failed on non-interactive process. Need Timed One Time Password(TOTP) 2FA for (%s).'%e.url)
|
||||
return
|
||||
print('Need Timed One Time Password(TOTP) 2FA for (%s):'%e.url)
|
||||
sys.stdout.write('\nEnter TOTP: ')
|
||||
adapter.totp = sys.stdin.readline().strip()
|
||||
# print('Login: `%s`, Password: `%s`' % (adapter.username, adapter.password))
|
||||
except exceptions.AdultCheckRequired:
|
||||
if options.interactive:
|
||||
print('Please confirm you are an adult in your locale: (y/n)?')
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ class FailedToLogin(Exception):
|
|||
else:
|
||||
return "Failed to Login for URL: (%s) with username: (%s)" % (self.url, self.username)
|
||||
|
||||
class NeedTimedOneTimePassword(Exception):
|
||||
def __init__(self,url):
|
||||
self.url=url
|
||||
|
||||
def __str__(self):
|
||||
return "Timed One Time Password(TOTP) required for 2 Factor Authentication(2FA): (%s) " % (self.url)
|
||||
|
||||
class AdultCheckRequired(Exception):
|
||||
def __init__(self,url):
|
||||
self.url=url
|
||||
|
|
|
|||
Loading…
Reference in a new issue