# -*- coding: utf-8 -*- """ Copyright (C) 2018-2018 plugin.video.youtube SPDX-License-Identifier: GPL-2.0-only See LICENSES/GPL-2.0-only for more information. """ from six.moves import BaseHTTPServer from six.moves.urllib.parse import parse_qs, urlparse from six.moves import range import json import os import re import requests import socket import xbmc import xbmcaddon import xbmcgui import xbmcvfs from .. import logger try: xbmc.translatePath = xbmcvfs.translatePath except AttributeError: pass class YouTubeRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def __init__(self, request, client_address, server): self.addon_id = 'plugin.video.youtube' addon = xbmcaddon.Addon(self.addon_id) whitelist_ips = addon.getSetting('kodion.http.ip.whitelist') whitelist_ips = ''.join(whitelist_ips.split()) self.whitelist_ips = whitelist_ips.split(',') self.local_ranges = ('10.', '172.16.', '192.168.', '127.0.0.1', 'localhost', '::1') self.chunk_size = 1024 * 64 try: self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id).decode('utf-8') except AttributeError: self.base_path = xbmc.translatePath('special://temp/%s' % self.addon_id) BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request, client_address, server) def connection_allowed(self): client_ip = self.client_address[0] log_lines = ['HTTPServer: Connection from |%s|' % client_ip] conn_allowed = client_ip.startswith(self.local_ranges) log_lines.append('Local range: |%s|' % str(conn_allowed)) if not conn_allowed: conn_allowed = client_ip in self.whitelist_ips log_lines.append('Whitelisted: |%s|' % str(conn_allowed)) if not conn_allowed: logger.log_debug('HTTPServer: Connection from |%s| not allowed' % client_ip) else: if self.path != '/ping': logger.log_debug(' '.join(log_lines)) return conn_allowed # noinspection PyPep8Naming def do_GET(self): addon = xbmcaddon.Addon('plugin.video.youtube') dash_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.mpd') == 'true' api_config_enabled = addon.getSetting('youtube.api.config.page') == 'true' if self.path == '/client_ip': client_json = json.dumps({"ip": "{ip}".format(ip=self.client_address[0])}) self.send_response(200) self.send_header('Content-Type', 'application/json; charset=utf-8') self.send_header('Content-Length', len(client_json)) self.end_headers() self.wfile.write(client_json.encode('utf-8')) if self.path != '/ping': logger.log_debug('HTTPServer: Request uri path |{proxy_path}|'.format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) else: if dash_proxy_enabled and self.path.endswith('.mpd'): file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) file_chunk = True logger.log_debug('HTTPServer: Request file path |{file_path}|'.format(file_path=file_path.encode('utf-8'))) try: with open(file_path, 'rb') as f: self.send_response(200) self.send_header('Content-Type', 'application/xml+dash') self.send_header('Content-Length', os.path.getsize(file_path)) self.end_headers() while file_chunk: file_chunk = f.read(self.chunk_size) if file_chunk: self.wfile.write(file_chunk) except IOError: response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8')) self.send_error(404, response) elif api_config_enabled and self.path == '/api': html = self.api_config_page() html = html.encode('utf-8') self.send_response(200) self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Content-Length', len(html)) self.end_headers() for chunk in self.get_chunks(html): self.wfile.write(chunk) elif api_config_enabled and self.path.startswith('/api_submit'): addon = xbmcaddon.Addon('plugin.video.youtube') i18n = addon.getLocalizedString xbmc.executebuiltin('Dialog.Close(addonsettings,true)') old_api_key = addon.getSetting('youtube.api.key') old_api_id = addon.getSetting('youtube.api.id') old_api_secret = addon.getSetting('youtube.api.secret') query = urlparse(self.path).query params = parse_qs(query) api_key = params.get('api_key', [None])[0] api_id = params.get('api_id', [None])[0] api_secret = params.get('api_secret', [None])[0] if api_key and api_id and api_secret: footer = i18n(30638) else: footer = u'' if re.search(r'api_key=(?:&|$)', query): api_key = '' if re.search(r'api_id=(?:&|$)', query): api_id = '' if re.search(r'api_secret=(?:&|$)', query): api_secret = '' updated = [] if api_key is not None and api_key != old_api_key: addon.setSetting('youtube.api.key', api_key) updated.append(i18n(30201)) if api_id is not None and api_id != old_api_id: addon.setSetting('youtube.api.id', api_id) updated.append(i18n(30202)) if api_secret is not None and api_secret != old_api_secret: updated.append(i18n(30203)) addon.setSetting('youtube.api.secret', api_secret) if addon.getSetting('youtube.api.key') and addon.getSetting('youtube.api.id') and \ addon.getSetting('youtube.api.secret'): enabled = i18n(30636) else: enabled = i18n(30637) if not updated: updated = i18n(30635) else: updated = i18n(30631) % u', '.join(updated) html = self.api_submit_page(updated, enabled, footer) html = html.encode('utf-8') self.send_response(200) self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Content-Length', len(html)) self.end_headers() for chunk in self.get_chunks(html): self.wfile.write(chunk) elif self.path == '/ping': self.send_error(204) else: self.send_error(501) # noinspection PyPep8Naming def do_HEAD(self): logger.log_debug('HTTPServer: Request uri path |{proxy_path}|'.format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) else: addon = xbmcaddon.Addon('plugin.video.youtube') dash_proxy_enabled = addon.getSetting('kodion.mpd.videos') == 'true' and addon.getSetting('kodion.video.quality.mpd') == 'true' if dash_proxy_enabled and self.path.endswith('.mpd'): file_path = os.path.join(self.base_path, self.path.strip('/').strip('\\')) if not os.path.isfile(file_path): response = 'File Not Found: |{proxy_path}| -> |{file_path}|'.format(proxy_path=self.path, file_path=file_path.encode('utf-8')) self.send_error(404, response) else: self.send_response(200) self.send_header('Content-Type', 'application/xml+dash') self.send_header('Content-Length', os.path.getsize(file_path)) self.end_headers() else: self.send_error(501) # noinspection PyPep8Naming def do_POST(self): logger.log_debug('HTTPServer: Request uri path |{proxy_path}|'.format(proxy_path=self.path)) if not self.connection_allowed(): self.send_error(403) elif self.path.startswith('/widevine'): license_url = xbmcgui.Window(10000).getProperty('plugin.video.youtube-license_url') license_token = xbmcgui.Window(10000).getProperty('plugin.video.youtube-license_token') if not license_url: self.send_error(404) return if not license_token: self.send_error(403) return size_limit = None length = int(self.headers['Content-Length']) post_data = self.rfile.read(length) li_headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Bearer %s' % license_token } result = requests.post(url=license_url, headers=li_headers, data=post_data, stream=True) response_length = int(result.headers.get('content-length')) content = result.raw.read(response_length) content_split = content.split('\r\n\r\n'.encode('utf-8')) response_header = content_split[0].decode('utf-8', 'ignore') response_body = content_split[1] response_length = len(response_body) match = re.search(r'^Authorized-Format-Types:\s*(?P.+?)\r*$', response_header, re.MULTILINE) if match: authorized_types = match.group('authorized_types').split(',') logger.log_debug('HTTPServer: Found authorized formats |{authorized_fmts}|'.format(authorized_fmts=authorized_types)) fmt_to_px = {'SD': (1280 * 528) - 1, 'HD720': 1280 * 720, 'HD': 7680 * 4320} if 'HD' in authorized_types: size_limit = fmt_to_px['HD'] elif 'HD720' in authorized_types: if xbmc.getCondVisibility('system.platform.android') == 1: size_limit = fmt_to_px['HD720'] else: size_limit = fmt_to_px['SD'] elif 'SD' in authorized_types: size_limit = fmt_to_px['SD'] self.send_response(200) if size_limit: self.send_header('X-Limit-Video', 'max={size_limit}px'.format(size_limit=str(size_limit))) for d in list(result.headers.items()): if re.match('^[Cc]ontent-[Ll]ength$', d[0]): self.send_header(d[0], response_length) else: self.send_header(d[0], d[1]) self.end_headers() for chunk in self.get_chunks(response_body): self.wfile.write(chunk) else: self.send_error(501) # noinspection PyShadowingBuiltins def log_message(self, format, *args): return def get_chunks(self, data): for i in range(0, len(data), self.chunk_size): yield data[i:i + self.chunk_size] @staticmethod def api_config_page(): addon = xbmcaddon.Addon('plugin.video.youtube') i18n = addon.getLocalizedString api_key = addon.getSetting('youtube.api.key') api_id = addon.getSetting('youtube.api.id') api_secret = addon.getSetting('youtube.api.secret') html = Pages().api_configuration.get('html') css = Pages().api_configuration.get('css') html = html.format(css=css, title=i18n(30634), api_key_head=i18n(30201), api_id_head=i18n(30202), api_secret_head=i18n(30203), api_id_value=api_id, api_key_value=api_key, api_secret_value=api_secret, submit=i18n(30630), header=i18n(30634)) return html @staticmethod def api_submit_page(updated_keys, enabled, footer): addon = xbmcaddon.Addon('plugin.video.youtube') i18n = addon.getLocalizedString html = Pages().api_submit.get('html') css = Pages().api_submit.get('css') html = html.format(css=css, title=i18n(30634), updated=updated_keys, enabled=enabled, footer=footer, header=i18n(30634)) return html class Pages(object): api_configuration = { 'html': u'\n\n' u'\n\t\n' u'\t{title}\n' u'\t\n' u'\n\n' u'\t
\n' u'\t
{header}
\n' u'\t
\n' u'\t\t\n' u'\t\t\n' u'\t\t\n' u'\t\t\n' u'\t
\n' u'\t
\n' u'\n', 'css': u'body {\n' u' background: #141718;\n' u'}\n' u'.center {\n' u' margin: auto;\n' u' width: 600px;\n' u' padding: 10px;\n' u'}\n' u'.config_form {\n' u' width: 575px;\n' u' height: 145px;\n' u' font-size: 16px;\n' u' background: #1a2123;\n' u' padding: 30px 30px 15px 30px;\n' u' border: 5px solid #1a2123;\n' u'}\n' u'h5 {\n' u' font-family: Arial, Helvetica, sans-serif;\n' u' font-size: 16px;\n' u' color: #fff;\n' u' font-weight: 600;\n' u' width: 575px;\n' u' height: 20px;\n' u' background: #0f84a5;\n' u' padding: 5px 30px 5px 30px;\n' u' border: 5px solid #0f84a5;\n' u' margin: 0px;\n' u'}\n' u'.config_form input[type=submit],\n' u'.config_form input[type=button],\n' u'.config_form input[type=text],\n' u'.config_form textarea,\n' u'.config_form label {\n' u' font-family: Arial, Helvetica, sans-serif;\n' u' font-size: 16px;\n' u' color: #fff;\n' u'}\n' u'.config_form label {\n' u' display:block;\n' u' margin-bottom: 10px;\n' u'}\n' u'.config_form label > span {\n' u' display: inline-block;\n' u' float: left;\n' u' width: 150px;\n' u'}\n' u'.config_form input[type=text] {\n' u' background: transparent;\n' u' border: none;\n' u' border-bottom: 1px solid #147a96;\n' u' width: 400px;\n' u' outline: none;\n' u' padding: 0px 0px 0px 0px;\n' u'}\n' u'.config_form input[type=text]:focus {\n' u' border-bottom: 1px dashed #0f84a5;\n' u'}\n' u'.config_form input[type=submit],\n' u'.config_form input[type=button] {\n' u' width: 150px;\n' u' background: #141718;\n' u' border: none;\n' u' padding: 8px 0px 8px 10px;\n' u' border-radius: 5px;\n' u' color: #fff;\n' u' margin-top: 10px\n' u'}\n' u'.config_form input[type=submit]:hover,\n' u'.config_form input[type=button]:hover {\n' u' background: #0f84a5;\n' u'}\n' } api_submit = { 'html': u'\n\n' u'\n\t\n' u'\t{title}\n' u'\t\n' u'\n\n' u'\t
\n' u'\t
{header}
\n' u'\t
\n' u'\t\t{updated}\n' u'\t\t{enabled}\n' u'\t\t \n' u'\t\t \n' u'\t\t \n' u'\t\t \n' u'\t\t
\n' u'\t\t\t{footer}\n' u'\t\t
\n' u'\t
\n' u'\t
\n' u'\n', 'css': u'body {\n' u' background: #141718;\n' u'}\n' u'.center {\n' u' margin: auto;\n' u' width: 600px;\n' u' padding: 10px;\n' u'}\n' u'.textcenter {\n' u' margin: auto;\n' u' width: 600px;\n' u' padding: 10px;\n' u' text-align: center;\n' u'}\n' u'.content {\n' u' width: 575px;\n' u' height: 145px;\n' u' background: #1a2123;\n' u' padding: 30px 30px 15px 30px;\n' u' border: 5px solid #1a2123;\n' u'}\n' u'h5 {\n' u' font-family: Arial, Helvetica, sans-serif;\n' u' font-size: 16px;\n' u' color: #fff;\n' u' font-weight: 600;\n' u' width: 575px;\n' u' height: 20px;\n' u' background: #0f84a5;\n' u' padding: 5px 30px 5px 30px;\n' u' border: 5px solid #0f84a5;\n' u' margin: 0px;\n' u'}\n' u'span {\n' u' font-family: Arial, Helvetica, sans-serif;\n' u' font-size: 16px;\n' u' color: #fff;\n' u' display: block;\n' u' float: left;\n' u' width: 575px;\n' u'}\n' u'small {\n' u' font-family: Arial, Helvetica, sans-serif;\n' u' font-size: 12px;\n' u' color: #fff;\n' u'}\n' } def get_http_server(address=None, port=None): addon_id = 'plugin.video.youtube' addon = xbmcaddon.Addon(addon_id) address = address if address else addon.getSetting('kodion.http.listen') address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '0.0.0.0' port = int(port) if port else 50152 try: server = BaseHTTPServer.HTTPServer((address, port), YouTubeRequestHandler) return server except socket.error as e: logger.log_debug('HTTPServer: Failed to start |{address}:{port}| |{response}|'.format(address=address, port=port, response=str(e))) xbmcgui.Dialog().notification(addon.getAddonInfo('name'), str(e), xbmc.translatePath('special://home/addons/{0!s}/icon.png'.format(addon.getAddonInfo('id'))), 5000, False) return None def is_httpd_live(address=None, port=None): addon_id = 'plugin.video.youtube' addon = xbmcaddon.Addon(addon_id) address = address if address else addon.getSetting('kodion.http.listen') address = '127.0.0.1' if address == '0.0.0.0' else address address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '127.0.0.1' port = int(port) if port else 50152 url = 'http://{address}:{port}/ping'.format(address=address, port=port) try: response = requests.get(url) result = response.status_code == 204 if not result: logger.log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response=response.status_code)) return result except: logger.log_debug('HTTPServer: Ping |{address}:{port}| |{response}|'.format(address=address, port=port, response='failed')) return False def get_client_ip_address(address=None, port=None): addon_id = 'plugin.video.youtube' addon = xbmcaddon.Addon(addon_id) address = address if address else addon.getSetting('kodion.http.listen') address = '127.0.0.1' if address == '0.0.0.0' else address address = address if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', address) else '127.0.0.1' port = int(port) if port else 50152 url = 'http://{address}:{port}/client_ip'.format(address=address, port=port) response = requests.get(url) ip_address = None if response.status_code == 200: response_json = response.json() if response_json: ip_address = response_json.get('ip') return ip_address