logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

goplay.py (17714B)


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