astrXbian/.install/.kodi/addons/plugin.video.youtube/resources/lib/youtube_plugin/kodion/utils/http_server.py

510 lines
21 KiB
Python

# -*- 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<authorized_types>.+?)\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'<!doctype html>\n<html>\n'
u'<head>\n\t<meta charset="utf-8">\n'
u'\t<title>{title}</title>\n'
u'\t<style>\n{css}\t</style>\n'
u'</head>\n<body>\n'
u'\t<div class="center">\n'
u'\t<h5>{header}</h5>\n'
u'\t<form action="/api_submit" class="config_form">\n'
u'\t\t<label for="api_key">\n'
u'\t\t<span>{api_key_head}</span><input type="text" name="api_key" value="{api_key_value}" size="50"/>\n'
u'\t\t</label>\n'
u'\t\t<label for="api_id">\n'
u'\t\t<span>{api_id_head}</span><input type="text" name="api_id" value="{api_id_value}" size="50"/>\n'
u'\t\t</label>\n'
u'\t\t<label for="api_secret">\n'
u'\t\t<span>{api_secret_head}</span><input type="text" name="api_secret" value="{api_secret_value}" size="50"/>\n'
u'\t\t</label>\n'
u'\t\t<input type="submit" value="{submit}">\n'
u'\t</form>\n'
u'\t</div>\n'
u'</body>\n</html>',
'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'<!doctype html>\n<html>\n'
u'<head>\n\t<meta charset="utf-8">\n'
u'\t<title>{title}</title>\n'
u'\t<style>\n{css}\t</style>\n'
u'</head>\n<body>\n'
u'\t<div class="center">\n'
u'\t<h5>{header}</h5>\n'
u'\t<div class="content">\n'
u'\t\t<span>{updated}</span>\n'
u'\t\t<span>{enabled}</span>\n'
u'\t\t<span>&nbsp;</span>\n'
u'\t\t<span>&nbsp;</span>\n'
u'\t\t<span>&nbsp;</span>\n'
u'\t\t<span>&nbsp;</span>\n'
u'\t\t<div class="textcenter">\n'
u'\t\t\t<span><small>{footer}</small></span>\n'
u'\t\t</div>\n'
u'\t</div>\n'
u'\t</div>\n'
u'</body>\n</html>',
'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