goplay.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. import base64
  2. import binascii
  3. import datetime as dt
  4. import hashlib
  5. import hmac
  6. import json
  7. import os
  8. from .common import InfoExtractor
  9. from ..utils import (
  10. ExtractorError,
  11. traverse_obj,
  12. unescapeHTML,
  13. )
  14. class GoPlayIE(InfoExtractor):
  15. _VALID_URL = r'https?://(www\.)?goplay\.be/video/([^/]+/[^/]+/|)(?P<display_id>[^/#]+)'
  16. _NETRC_MACHINE = 'goplay'
  17. _TESTS = [{
  18. 'url': 'https://www.goplay.be/video/de-container-cup/de-container-cup-s3/de-container-cup-s3-aflevering-2#autoplay',
  19. 'info_dict': {
  20. 'id': '9c4214b8-e55d-4e4b-a446-f015f6c6f811',
  21. 'ext': 'mp4',
  22. 'title': 'S3 - Aflevering 2',
  23. 'series': 'De Container Cup',
  24. 'season': 'Season 3',
  25. 'season_number': 3,
  26. 'episode': 'Episode 2',
  27. 'episode_number': 2,
  28. },
  29. 'skip': 'This video is only available for registered users',
  30. }, {
  31. 'url': 'https://www.goplay.be/video/a-family-for-thr-holidays-s1-aflevering-1#autoplay',
  32. 'info_dict': {
  33. 'id': '74e3ed07-748c-49e4-85a0-393a93337dbf',
  34. 'ext': 'mp4',
  35. 'title': 'A Family for the Holidays',
  36. },
  37. 'skip': 'This video is only available for registered users',
  38. }, {
  39. 'url': 'https://www.goplay.be/video/de-mol/de-mol-s11/de-mol-s11-aflevering-1#autoplay',
  40. 'info_dict': {
  41. 'id': '03eb8f2f-153e-41cb-9805-0d3a29dab656',
  42. 'ext': 'mp4',
  43. 'title': 'S11 - Aflevering 1',
  44. 'episode': 'Episode 1',
  45. 'series': 'De Mol',
  46. 'season_number': 11,
  47. 'episode_number': 1,
  48. 'season': 'Season 11',
  49. },
  50. 'params': {
  51. 'skip_download': True,
  52. },
  53. 'skip': 'This video is only available for registered users',
  54. }]
  55. _id_token = None
  56. def _perform_login(self, username, password):
  57. self.report_login()
  58. aws = AwsIdp(ie=self, pool_id='eu-west-1_dViSsKM5Y', client_id='6s1h851s8uplco5h6mqh1jac8m')
  59. self._id_token, _ = aws.authenticate(username=username, password=password)
  60. def _real_initialize(self):
  61. if not self._id_token:
  62. raise self.raise_login_required(method='password')
  63. def _real_extract(self, url):
  64. url, display_id = self._match_valid_url(url).group(0, 'display_id')
  65. webpage = self._download_webpage(url, display_id)
  66. video_data_json = self._html_search_regex(r'<div\s+data-hero="([^"]+)"', webpage, 'video_data')
  67. video_data = self._parse_json(unescapeHTML(video_data_json), display_id).get('data')
  68. movie = video_data.get('movie')
  69. if movie:
  70. video_id = movie['videoUuid']
  71. info_dict = {
  72. 'title': movie.get('title'),
  73. }
  74. else:
  75. episode = traverse_obj(video_data, ('playlists', ..., 'episodes', lambda _, v: v['pageInfo']['url'] == url), get_all=False)
  76. video_id = episode['videoUuid']
  77. info_dict = {
  78. 'title': episode.get('episodeTitle'),
  79. 'series': traverse_obj(episode, ('program', 'title')),
  80. 'season_number': episode.get('seasonNumber'),
  81. 'episode_number': episode.get('episodeNumber'),
  82. }
  83. api = self._download_json(
  84. f'https://api.goplay.be/web/v1/videos/long-form/{video_id}',
  85. video_id, headers={
  86. 'Authorization': f'Bearer {self._id_token}',
  87. **self.geo_verification_headers(),
  88. })
  89. if 'manifestUrls' in api:
  90. formats, subtitles = self._extract_m3u8_formats_and_subtitles(
  91. api['manifestUrls']['hls'], video_id, ext='mp4', m3u8_id='HLS')
  92. else:
  93. if 'ssai' not in api:
  94. raise ExtractorError('expecting Google SSAI stream')
  95. ssai_content_source_id = api['ssai']['contentSourceID']
  96. ssai_video_id = api['ssai']['videoID']
  97. dai = self._download_json(
  98. f'https://dai.google.com/ondemand/dash/content/{ssai_content_source_id}/vid/{ssai_video_id}/streams',
  99. video_id, data=b'{"api-key":"null"}',
  100. headers={'content-type': 'application/json'})
  101. periods = self._extract_mpd_periods(dai['stream_manifest'], video_id)
  102. # skip pre-roll and mid-roll ads
  103. periods = [p for p in periods if '-ad-' not in p['id']]
  104. formats, subtitles = self._merge_mpd_periods(periods)
  105. info_dict.update({
  106. 'id': video_id,
  107. 'formats': formats,
  108. 'subtitles': subtitles,
  109. })
  110. return info_dict
  111. # Taken from https://github.com/add-ons/plugin.video.viervijfzes/blob/master/resources/lib/viervijfzes/auth_awsidp.py
  112. # Released into Public domain by https://github.com/michaelarnauts
  113. class InvalidLoginException(ExtractorError):
  114. """ The login credentials are invalid """
  115. class AuthenticationException(ExtractorError):
  116. """ Something went wrong while logging in """
  117. class AwsIdp:
  118. """ AWS Identity Provider """
  119. def __init__(self, ie, pool_id, client_id):
  120. """
  121. :param InfoExtrator ie: The extractor that instantiated this class.
  122. :param str pool_id: The AWS user pool to connect to (format: <region>_<poolid>).
  123. E.g.: eu-west-1_aLkOfYN3T
  124. :param str client_id: The client application ID (the ID of the application connecting)
  125. """
  126. self.ie = ie
  127. self.pool_id = pool_id
  128. if '_' not in self.pool_id:
  129. raise ValueError('Invalid pool_id format. Should be <region>_<poolid>.')
  130. self.client_id = client_id
  131. self.region = self.pool_id.split('_')[0]
  132. self.url = f'https://cognito-idp.{self.region}.amazonaws.com/'
  133. # Initialize the values
  134. # https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L22
  135. self.n_hex = (
  136. 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1'
  137. '29024E088A67CC74020BBEA63B139B22514A08798E3404DD'
  138. 'EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245'
  139. 'E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED'
  140. 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D'
  141. 'C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F'
  142. '83655D23DCA3AD961C62F356208552BB9ED529077096966D'
  143. '670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B'
  144. 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9'
  145. 'DE2BCBF6955817183995497CEA956AE515D2261898FA0510'
  146. '15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64'
  147. 'ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7'
  148. 'ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B'
  149. 'F12FFA06D98A0864D87602733EC86A64521F2B18177B200C'
  150. 'BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31'
  151. '43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF')
  152. # https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L49
  153. self.g_hex = '2'
  154. self.info_bits = bytearray('Caldera Derived Key', 'utf-8')
  155. self.big_n = self.__hex_to_long(self.n_hex)
  156. self.g = self.__hex_to_long(self.g_hex)
  157. self.k = self.__hex_to_long(self.__hex_hash('00' + self.n_hex + '0' + self.g_hex))
  158. self.small_a_value = self.__generate_random_small_a()
  159. self.large_a_value = self.__calculate_a()
  160. def authenticate(self, username, password):
  161. """ Authenticate with a username and password. """
  162. # Step 1: First initiate an authentication request
  163. auth_data_dict = self.__get_authentication_request(username)
  164. auth_data = json.dumps(auth_data_dict).encode()
  165. auth_headers = {
  166. 'X-Amz-Target': 'AWSCognitoIdentityProviderService.InitiateAuth',
  167. 'Accept-Encoding': 'identity',
  168. 'Content-Type': 'application/x-amz-json-1.1',
  169. }
  170. auth_response_json = self.ie._download_json(
  171. self.url, None, data=auth_data, headers=auth_headers,
  172. note='Authenticating username', errnote='Invalid username')
  173. challenge_parameters = auth_response_json.get('ChallengeParameters')
  174. if auth_response_json.get('ChallengeName') != 'PASSWORD_VERIFIER':
  175. raise AuthenticationException(auth_response_json['message'])
  176. # Step 2: Respond to the Challenge with a valid ChallengeResponse
  177. challenge_request = self.__get_challenge_response_request(challenge_parameters, password)
  178. challenge_data = json.dumps(challenge_request).encode()
  179. challenge_headers = {
  180. 'X-Amz-Target': 'AWSCognitoIdentityProviderService.RespondToAuthChallenge',
  181. 'Content-Type': 'application/x-amz-json-1.1',
  182. }
  183. auth_response_json = self.ie._download_json(
  184. self.url, None, data=challenge_data, headers=challenge_headers,
  185. note='Authenticating password', errnote='Invalid password')
  186. if 'message' in auth_response_json:
  187. raise InvalidLoginException(auth_response_json['message'])
  188. return (
  189. auth_response_json['AuthenticationResult']['IdToken'],
  190. auth_response_json['AuthenticationResult']['RefreshToken'],
  191. )
  192. def __get_authentication_request(self, username):
  193. """
  194. :param str username: The username to use
  195. :return: A full Authorization request.
  196. :rtype: dict
  197. """
  198. return {
  199. 'AuthParameters': {
  200. 'USERNAME': username,
  201. 'SRP_A': self.__long_to_hex(self.large_a_value),
  202. },
  203. 'AuthFlow': 'USER_SRP_AUTH',
  204. 'ClientId': self.client_id,
  205. }
  206. def __get_challenge_response_request(self, challenge_parameters, password):
  207. """ Create a Challenge Response Request object.
  208. :param dict[str,str|imt] challenge_parameters: The parameters for the challenge.
  209. :param str password: The password.
  210. :return: A valid and full request data object to use as a response for a challenge.
  211. :rtype: dict
  212. """
  213. user_id = challenge_parameters['USERNAME']
  214. user_id_for_srp = challenge_parameters['USER_ID_FOR_SRP']
  215. srp_b = challenge_parameters['SRP_B']
  216. salt = challenge_parameters['SALT']
  217. secret_block = challenge_parameters['SECRET_BLOCK']
  218. timestamp = self.__get_current_timestamp()
  219. # Get a HKDF key for the password, SrpB and the Salt
  220. hkdf = self.__get_hkdf_key_for_password(
  221. user_id_for_srp,
  222. password,
  223. self.__hex_to_long(srp_b),
  224. salt,
  225. )
  226. secret_block_bytes = base64.standard_b64decode(secret_block)
  227. # the message is a combo of the pool_id, provided SRP userId, the Secret and Timestamp
  228. msg = \
  229. bytearray(self.pool_id.split('_')[1], 'utf-8') + \
  230. bytearray(user_id_for_srp, 'utf-8') + \
  231. bytearray(secret_block_bytes) + \
  232. bytearray(timestamp, 'utf-8')
  233. hmac_obj = hmac.new(hkdf, msg, digestmod=hashlib.sha256)
  234. signature_string = base64.standard_b64encode(hmac_obj.digest()).decode('utf-8')
  235. return {
  236. 'ChallengeResponses': {
  237. 'USERNAME': user_id,
  238. 'TIMESTAMP': timestamp,
  239. 'PASSWORD_CLAIM_SECRET_BLOCK': secret_block,
  240. 'PASSWORD_CLAIM_SIGNATURE': signature_string,
  241. },
  242. 'ChallengeName': 'PASSWORD_VERIFIER',
  243. 'ClientId': self.client_id,
  244. }
  245. def __get_hkdf_key_for_password(self, username, password, server_b_value, salt):
  246. """ Calculates the final hkdf based on computed S value, and computed U value and the key.
  247. :param str username: Username.
  248. :param str password: Password.
  249. :param int server_b_value: Server B value.
  250. :param int salt: Generated salt.
  251. :return Computed HKDF value.
  252. :rtype: object
  253. """
  254. u_value = self.__calculate_u(self.large_a_value, server_b_value)
  255. if u_value == 0:
  256. raise ValueError('U cannot be zero.')
  257. username_password = '{}{}:{}'.format(self.pool_id.split('_')[1], username, password)
  258. username_password_hash = self.__hash_sha256(username_password.encode())
  259. x_value = self.__hex_to_long(self.__hex_hash(self.__pad_hex(salt) + username_password_hash))
  260. g_mod_pow_xn = pow(self.g, x_value, self.big_n)
  261. int_value2 = server_b_value - self.k * g_mod_pow_xn
  262. s_value = pow(int_value2, self.small_a_value + u_value * x_value, self.big_n)
  263. return self.__compute_hkdf(
  264. bytearray.fromhex(self.__pad_hex(s_value)),
  265. bytearray.fromhex(self.__pad_hex(self.__long_to_hex(u_value))),
  266. )
  267. def __compute_hkdf(self, ikm, salt):
  268. """ Standard hkdf algorithm
  269. :param {Buffer} ikm Input key material.
  270. :param {Buffer} salt Salt value.
  271. :return {Buffer} Strong key material.
  272. """
  273. prk = hmac.new(salt, ikm, hashlib.sha256).digest()
  274. info_bits_update = self.info_bits + bytearray(chr(1), 'utf-8')
  275. hmac_hash = hmac.new(prk, info_bits_update, hashlib.sha256).digest()
  276. return hmac_hash[:16]
  277. def __calculate_u(self, big_a, big_b):
  278. """ Calculate the client's value U which is the hash of A and B
  279. :param int big_a: Large A value.
  280. :param int big_b: Server B value.
  281. :return Computed U value.
  282. :rtype: int
  283. """
  284. u_hex_hash = self.__hex_hash(self.__pad_hex(big_a) + self.__pad_hex(big_b))
  285. return self.__hex_to_long(u_hex_hash)
  286. def __generate_random_small_a(self):
  287. """ Helper function to generate a random big integer
  288. :return a random value.
  289. :rtype: int
  290. """
  291. random_long_int = self.__get_random(128)
  292. return random_long_int % self.big_n
  293. def __calculate_a(self):
  294. """ Calculate the client's public value A = g^a%N with the generated random number a
  295. :return Computed large A.
  296. :rtype: int
  297. """
  298. big_a = pow(self.g, self.small_a_value, self.big_n)
  299. # safety check
  300. if (big_a % self.big_n) == 0:
  301. raise ValueError('Safety check for A failed')
  302. return big_a
  303. @staticmethod
  304. def __long_to_hex(long_num):
  305. return f'{long_num:x}'
  306. @staticmethod
  307. def __hex_to_long(hex_string):
  308. return int(hex_string, 16)
  309. @staticmethod
  310. def __hex_hash(hex_string):
  311. return AwsIdp.__hash_sha256(bytearray.fromhex(hex_string))
  312. @staticmethod
  313. def __hash_sha256(buf):
  314. """AuthenticationHelper.hash"""
  315. digest = hashlib.sha256(buf).hexdigest()
  316. return (64 - len(digest)) * '0' + digest
  317. @staticmethod
  318. def __pad_hex(long_int):
  319. """ Converts a Long integer (or hex string) to hex format padded with zeroes for hashing
  320. :param int|str long_int: Number or string to pad.
  321. :return Padded hex string.
  322. :rtype: str
  323. """
  324. if not isinstance(long_int, str):
  325. hash_str = AwsIdp.__long_to_hex(long_int)
  326. else:
  327. hash_str = long_int
  328. if len(hash_str) % 2 == 1:
  329. hash_str = f'0{hash_str}'
  330. elif hash_str[0] in '89ABCDEFabcdef':
  331. hash_str = f'00{hash_str}'
  332. return hash_str
  333. @staticmethod
  334. def __get_random(nbytes):
  335. random_hex = binascii.hexlify(os.urandom(nbytes))
  336. return AwsIdp.__hex_to_long(random_hex)
  337. @staticmethod
  338. def __get_current_timestamp():
  339. """ Creates a timestamp with the correct English format.
  340. :return: timestamp in format 'Sun Jan 27 19:00:04 UTC 2019'
  341. :rtype: str
  342. """
  343. # We need US only data, so we cannot just do a strftime:
  344. # Sun Jan 27 19:00:04 UTC 2019
  345. months = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
  346. days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  347. time_now = dt.datetime.now(dt.timezone.utc)
  348. format_string = f'{days[time_now.weekday()]} {months[time_now.month]} {time_now.day} %H:%M:%S UTC %Y'
  349. return time_now.strftime(format_string)
  350. def __str__(self):
  351. return 'AWS IDP Client for:\nRegion: {}\nPoolId: {}\nAppId: {}'.format(
  352. self.region, self.pool_id.split('_')[1], self.client_id,
  353. )