Преглед изворни кода

[fixed] Login stopped working because the added reCAPTCHA. Fixes #2.

TnS-hun пре 6 година
родитељ
комит
be5ffd4e5d
2 измењених фајлова са 54 додато и 24 уклоњено
  1. 29 22
      kobo-book-downloader/Kobo.py
  2. 25 2
      kobo-book-downloader/__main__.py

+ 29 - 22
kobo-book-downloader/Kobo.py

@@ -45,7 +45,7 @@ def ReauthenticationHook( r, *args, **kwargs ):
 
 class Kobo:
 	Affiliate = "Kobo"
-	ApplicationVersion = "7.1.21543"
+	ApplicationVersion = "8.11.24971"
 	DefaultPlatformId = "00000000-0000-0000-0000-000000004000"
 	DisplayProfile = "Android"
 
@@ -135,43 +135,50 @@ class Kobo:
 		jsonResponse = response.json()
 		self.InitializationSettings = jsonResponse[ "Resources" ]
 
-	def __GetAuthenticationUrlFromLoginPage( self ) -> str:
-		signInPageUrl = self.InitializationSettings[ "sign_in_page" ]
+	def __GetExtraLoginParameters( self ) -> Tuple[ str, str, str ]:
+		signInUrl = self.InitializationSettings[ "sign_in_page" ]
 
 		params = {
 			"wsa": Kobo.Affiliate,
-			"wscf": "kepub",
 			"pwsav": Kobo.ApplicationVersion,
 			"pwspid": Kobo.DefaultPlatformId,
 			"pwsdid": Globals.Settings.DeviceId
 		}
 
-		response = self.Session.get( signInPageUrl, params = params )
+		response = self.Session.get( signInUrl, params = params )
 		response.raise_for_status()
 		htmlResponse = response.text
 
-		match = re.search( r"""<form action="(/[^"]+/Authenticate\?[^"]+)" method="post">""", htmlResponse )
+		# The link can be found in the response ('<a class="kobo-link partner-option kobo"') but this will do for now.
+		parsed = urllib.parse.urlparse( signInUrl )
+		koboSignInUrl = parsed._replace( query = None, path = "/ww/en/signin/signin/kobo" ).geturl()
+
+		match = re.search( r'href="/ww/en/signin/signin/kobo\?workflowId=([^"]+)"', htmlResponse )
+		if match is None:
+			raise KoboException( "Can't find the workflow ID in the login form. The page format might have changed." )
+		workflowId = html.unescape( match.group( 1 ) )
+
+		match = re.search( r"""<input name="__RequestVerificationToken" type="hidden" value="([^"]+)" />""", htmlResponse )
 		if match is None:
-			raise KoboException( "Can't find login form. The page format might have changed." )
+			raise KoboException( "Can't find the request verification token in the login form. The page format might have changed." )
+		requestVerificationToken = html.unescape( match.group( 1 ) )
 
-		authenticationUrl = match.group( 1 )
-		authenticationUrl = html.unescape( authenticationUrl )
-		authenticationUrl = urllib.parse.urljoin( signInPageUrl, authenticationUrl )
-		return authenticationUrl
+		return koboSignInUrl, workflowId, requestVerificationToken
 
-	def Login( self, email: str, password: str ) -> None:
-		authenticationUrl = self.__GetAuthenticationUrlFromLoginPage()
+	def Login( self, email: str, password: str, captcha: str ) -> None:
+		signInUrl, workflowId, requestVerificationToken = self.__GetExtraLoginParameters()
 
 		postData = {
-			"EditModel.Email": email,
-			"EditModel.Password": password,
-			"EditModel.AuthenticationAction": "Authenticate",
-			"AffiliateObject.Name": Kobo.Affiliate,
-			"IsFTE": "False",
-			"RequireSharedToken": "False"
+			"LogInModel.WorkflowId": workflowId,
+			"LogInModel.Provider": Kobo.Affiliate,
+			"ReturnUrl": "",
+			"__RequestVerificationToken": requestVerificationToken,
+			"LogInModel.UserName": email,
+			"LogInModel.Password": password,
+			"g-recaptcha-response": captcha
 		}
 
-		response = self.Session.post( authenticationUrl, data = postData )
+		response = self.Session.post( signInUrl, data = postData )
 		response.raise_for_status()
 		htmlResponse = response.text
 
@@ -197,7 +204,7 @@ class Kobo:
 		jsonResponse = response.json()
 		return jsonResponse
 
-	def __GetMyBookListPage( self, syncToken: str ) -> Tuple[ dict, str ]:
+	def __GetMyBookListPage( self, syncToken: str ) -> Tuple[ list, str ]:
 		url = self.InitializationSettings[ "library_sync" ]
 		headers = Kobo.GetHeaderWithAccessToken()
 		hooks = Kobo.__GetReauthenticationHook()
@@ -216,7 +223,7 @@ class Kobo:
 
 		return bookList, syncToken
 
-	def GetMyBookList( self ) -> dict:
+	def GetMyBookList( self ) -> list:
 		# The "library_sync" name and the synchronization tokens make it somewhat suspicious that we should use
 		# "library_items" instead to get the My Books list, but "library_items" gives back less info (even with the
 		# embed=ProductMetadata query parameter set).

+ 25 - 2
kobo-book-downloader/__main__.py

@@ -5,7 +5,7 @@ from Settings import Settings
 
 import argparse
 
-def Initialize():
+def Initialize() -> None:
 	Globals.Kobo = Kobo()
 	Globals.Settings = Settings()
 
@@ -17,7 +17,30 @@ def Initialize():
 	if not Globals.Settings.IsLoggedIn():
 		email = input( "Waiting for your input. You can use Shift+Insert to paste from the clipboard. Ctrl+C aborts the program.\n\nKobo e-mail: " )
 		password = input( "Kobo password: " )
-		Globals.Kobo.Login( email, password )
+
+		print( """
+Open https://authorize.kobo.com/signin in a private/incognito window in your browser, wait till the page
+loads (do not login!) then open the developer tools (use F12 in Firefox/Chrome), select the console tab,
+and paste the following code there and then press Enter there in the browser.
+
+var newCaptchaDiv = document.createElement( "div" );
+newCaptchaDiv.id = "new-grecaptcha-container";
+document.getElementById( "grecaptcha-container" ).insertAdjacentElement( "afterend", newCaptchaDiv );
+grecaptcha.render( newCaptchaDiv.id, {
+	sitekey: "6LeEbUwUAAAAADJxtlhMsvgnR7SsFpMm4sirr1CJ",
+	callback: function( response ) { console.log( "Captcha response:" ); console.log( response ); }
+} );
+
+A captcha should show up below the Sign-in form. Once you solve the captcha its response will be written
+below the pasted code in the browser's console. Copy the response (the line below "Captcha response:")
+and paste it here.
+""" )
+
+		captcha = input( "Captcha response: " ).strip()
+
+		print( "" )
+
+		Globals.Kobo.Login( email, password, captcha )
 
 def Main() -> None:
 	argumentParser = argparse.ArgumentParser( add_help = False )