from Globals import Globals from KoboDrmRemover import KoboDrmRemover import requests from typing import Dict, Tuple import base64 import html import os import re import urllib import uuid class KoboException( Exception ): pass # The hook's workflow is based on this: # https://github.com/requests/toolbelt/blob/master/requests_toolbelt/auth/http_proxy_digest.py def ReauthenticationHook( r, *args, **kwargs ): if r.status_code != requests.codes.unauthorized: # 401 return print( "Refreshing expired authentication token" ) # Consume content and release the original connection to allow our new request to reuse the same one. r.content r.close() prep = r.request.copy() # Refresh the authentication token and use it. Globals.Kobo.RefreshAuthentication() headers = Kobo.GetHeaderWithAccessToken() prep.headers[ "Authorization" ] = headers[ "Authorization" ] # Don't retry to reauthenticate this request again. prep.deregister_hook( "response", ReauthenticationHook ) # Resend the failed request. _r = r.connection.send( prep, **kwargs ) _r.history.append( r ) _r.request = prep return _r class Kobo: Affiliate = "Kobo" ApplicationVersion = "7.1.21543" DefaultPlatformId = "00000000-0000-0000-0000-000000004000" DisplayProfile = "Android" def __init__( self ): self.InitializationSettings = {} self.Session = requests.session() # This could be added to the session but then we would need to add { "Authorization": None } headers to all other # functions that doesn't need authorization. @staticmethod def GetHeaderWithAccessToken() -> dict: authorization = "Bearer " + Globals.Settings.AccessToken headers = { "Authorization": authorization } return headers # This could be added to the session too. See the comment at GetHeaderWithAccessToken. @staticmethod def __GetReauthenticationHook() -> dict: return { "response": ReauthenticationHook } # The initial device authentication request for a non-logged in user doesn't require a user key, and the returned # user key can't be used for anything. def AuthenticateDevice( self, userKey: str = "" ) -> None: if len( Globals.Settings.DeviceId ) == 0: Globals.Settings.DeviceId = str( uuid.uuid4() ) Globals.Settings.AccessToken = "" Globals.Settings.RefreshToken = "" postData = { "AffiliateName": Kobo.Affiliate, "AppVersion": Kobo.ApplicationVersion, "ClientKey": base64.b64encode( Kobo.DefaultPlatformId.encode() ).decode(), "DeviceId": Globals.Settings.DeviceId, "PlatformId": Kobo.DefaultPlatformId } if len( userKey ) > 0: postData[ "UserKey" ] = userKey response = self.Session.post( "https://storeapi.kobo.com/v1/auth/device", json = postData ) response.raise_for_status() jsonResponse = response.json() if jsonResponse[ "TokenType" ] != "Bearer": raise KoboException( "Device authentication returned with an unsupported token type: '%s'" % jsonResponse[ "TokenType" ] ) Globals.Settings.AccessToken = jsonResponse[ "AccessToken" ] Globals.Settings.RefreshToken = jsonResponse[ "RefreshToken" ] if not Globals.Settings.AreAuthenticationSettingsSet(): raise KoboException( "Authentication settings are not set after device authentication." ) if len( userKey ) > 0: Globals.Settings.UserKey = jsonResponse[ "UserKey" ] Globals.Settings.Save() def RefreshAuthentication( self ) -> None: headers = Kobo.GetHeaderWithAccessToken() postData = { "AppVersion": Kobo.ApplicationVersion, "ClientKey": base64.b64encode( Kobo.DefaultPlatformId.encode() ).decode(), "PlatformId": Kobo.DefaultPlatformId, "RefreshToken": Globals.Settings.RefreshToken } # The reauthentication hook is intentionally not set. response = self.Session.post( "https://storeapi.kobo.com/v1/auth/refresh", json = postData, headers = headers ) response.raise_for_status() jsonResponse = response.json() if jsonResponse[ "TokenType" ] != "Bearer": raise KoboException( "Authentication refresh returned with an unsupported token type: '%s'" % jsonResponse[ "TokenType" ] ) Globals.Settings.AccessToken = jsonResponse[ "AccessToken" ] Globals.Settings.RefreshToken = jsonResponse[ "RefreshToken" ] if not Globals.Settings.AreAuthenticationSettingsSet(): raise KoboException( "Authentication settings are not set after authentication refresh." ) Globals.Settings.Save() def LoadInitializationSettings( self ) -> None: headers = Kobo.GetHeaderWithAccessToken() hooks = Kobo.__GetReauthenticationHook() response = self.Session.get( "https://storeapi.kobo.com/v1/initialization", headers = headers, hooks = hooks ) response.raise_for_status() jsonResponse = response.json() self.InitializationSettings = jsonResponse[ "Resources" ] def __GetAuthenticationUrlFromLoginPage( self ) -> str: signInPageUrl = 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.raise_for_status() htmlResponse = response.text match = re.search( r"""