Initial commit: xStream Downloader
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/ParameterHandler.py
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/__init__.py
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,193 @@
|
||||
# Echte xStream-Handler (Standard-Lib only)
|
||||
import re
|
||||
from urllib.parse import quote, urlencode
|
||||
import gzip, ssl
|
||||
from urllib.request import Request, urlopen, HTTPCookieProcessor, build_opener, HTTPSHandler
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
class _FakeConfig:
|
||||
_settings = {
|
||||
'cacheTime': '1', 'requestTimeout': '15',
|
||||
'bypassDNSlock': 'false', 'volatileHtmlCache': 'false',
|
||||
}
|
||||
def getAddonInfo(self, k):
|
||||
return {'profile': '/tmp', 'id': 'xstream', 'name': 'xStream',
|
||||
'version': '1.0.0', 'icon': '', 'author': '', 'description': ''}.get(k, '')
|
||||
def getSetting(self, k): return self._settings.get(k, '')
|
||||
def setSetting(self, k, v): self._settings[k] = str(v)
|
||||
def getLocalizedString(self, k): return f'[{k}]'
|
||||
|
||||
class _FakeCache:
|
||||
def get(self, key, cache_time): return None
|
||||
def set(self, key, data): pass
|
||||
def clear(self): pass
|
||||
|
||||
class _FakeLogger:
|
||||
def info(self, *a, **k): pass
|
||||
def debug(self, *a, **k): pass
|
||||
def error(self, *a, **k): pass
|
||||
|
||||
def _infoDialog(*a, **k): pass
|
||||
|
||||
|
||||
class cRequestHandler:
|
||||
persistent_openers = {}
|
||||
|
||||
UA_LIST = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15',
|
||||
]
|
||||
|
||||
def __init__(self, sUrl='', caching=True, ignoreErrors=False, method='GET',
|
||||
data=None, compression=True, jspost=False, ssl_verify=False):
|
||||
self._sUrl = str(sUrl) if sUrl else ''
|
||||
self._sRealUrl = ''
|
||||
self._USER_AGENT = self.UA_LIST[0]
|
||||
self._aParameters = {}
|
||||
self._headerEntries = {}
|
||||
self._Status = ''
|
||||
self._sResponseHeader = ''
|
||||
self._ssl_verify = ssl_verify
|
||||
self.caching = caching
|
||||
self.method = method
|
||||
self.data = data
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self.compression = compression
|
||||
self.jspost = jspost
|
||||
self.requestTimeout = 15
|
||||
self.__bRemoveNewLines = True
|
||||
self.__bRemoveBreakLines = True
|
||||
self.isMemoryCacheActive = False
|
||||
self.__setDefaultHeader()
|
||||
|
||||
def __setDefaultHeader(self):
|
||||
self.addHeaderEntry('User-Agent', self._USER_AGENT)
|
||||
self.addHeaderEntry('Accept', '*/*')
|
||||
self.addHeaderEntry('Accept-Language', 'en-US,en;q=0.5')
|
||||
if self.compression:
|
||||
self.addHeaderEntry('Accept-Encoding', 'gzip, deflate')
|
||||
|
||||
def removeNewLines(self, b): self.__bRemoveNewLines = b
|
||||
def removeBreakLines(self, b): self.__bRemoveBreakLines = b
|
||||
|
||||
def addHeaderEntry(self, k, v): self._headerEntries[k] = v
|
||||
def addParameters(self, k, v, Quote=False):
|
||||
self._aParameters[k] = v if not Quote else quote(str(v))
|
||||
def getHeaderEntry(self, k): return self._headerEntries.get(k)
|
||||
def getResponseHeader(self): return self._sResponseHeader
|
||||
def getRealUrl(self): return self._sRealUrl or self._sUrl
|
||||
def getStatus(self): return self._Status
|
||||
def getUrl(self): return self._sUrl
|
||||
def setUrl(self, url):
|
||||
self._sUrl = str(url)
|
||||
return self
|
||||
def ignoreDiscard(self, v): pass
|
||||
def ignoreExpired(self, v): pass
|
||||
|
||||
def getHtml(self, encoding='utf-8'):
|
||||
if not self._sUrl or self._sUrl in ('False', 'None', ''):
|
||||
return ''
|
||||
try:
|
||||
url = self._sUrl
|
||||
if self._aParameters and self.method == 'GET':
|
||||
sep = '&' if '?' in url else '?'
|
||||
url = url + sep + urlencode(self._aParameters)
|
||||
|
||||
req = Request(url, data=self.data, method=self.method)
|
||||
for k, v in self._headerEntries.items():
|
||||
try: req.add_header(k, v)
|
||||
except: pass
|
||||
|
||||
ctx = None
|
||||
if self._ssl_verify is False:
|
||||
ctx = ssl._create_unverified_context()
|
||||
|
||||
opener = build_opener(HTTPCookieProcessor())
|
||||
with opener.open(req, timeout=self.requestTimeout) as resp:
|
||||
self._sRealUrl = resp.geturl()
|
||||
self._sResponseHeader = str(resp.headers)
|
||||
data = resp.read()
|
||||
|
||||
if self.compression and 'gzip' in resp.headers.get('Content-Encoding', ''):
|
||||
try: data = gzip.decompress(data)
|
||||
except: pass
|
||||
|
||||
text = data.decode(encoding, errors='replace')
|
||||
|
||||
if self.__bRemoveBreakLines:
|
||||
text = re.sub(r'[\r\n]+', '\n', text)
|
||||
if self.__bRemoveNewLines:
|
||||
text = re.sub(r'\n+', ' ', text)
|
||||
|
||||
return text
|
||||
|
||||
except HTTPError as e:
|
||||
self._Status = str(e.code)
|
||||
return f'<!-- HTTPError {e.code} -->' if not self.ignoreErrors else ''
|
||||
except URLError as e:
|
||||
self._Status = str(e.reason)
|
||||
return f'<!-- URLError {e.reason} -->' if not self.ignoreErrors else ''
|
||||
except Exception as e:
|
||||
self._Status = str(e)
|
||||
return f'<!-- Error {e} -->' if not self.ignoreErrors else ''
|
||||
|
||||
def getJson(self):
|
||||
import json
|
||||
text = self.getHtml()
|
||||
if text.strip().startswith('<!--'): return {}
|
||||
try: return json.loads(text)
|
||||
except: return {}
|
||||
|
||||
|
||||
class cParser:
|
||||
@staticmethod
|
||||
def urlEncode(params):
|
||||
if isinstance(params, dict): return urlencode(params)
|
||||
return quote(str(params) if params else '')
|
||||
|
||||
@staticmethod
|
||||
def parse(source, pattern, flags=0):
|
||||
return re.findall(pattern, source, flags)
|
||||
|
||||
@staticmethod
|
||||
def replace(pattern, replacement, source, count=0):
|
||||
return re.sub(pattern, replacement, source, count)
|
||||
|
||||
@staticmethod
|
||||
def searchSingle(result, searchPattern, outputParam=None, source=None):
|
||||
text = source if source is not None else (str(result) if result else '')
|
||||
match = re.search(searchPattern, text, re.S)
|
||||
if not match: return None
|
||||
return match.group(outputParam) if outputParam and outputParam <= match.lastindex else match.group(0)
|
||||
|
||||
@staticmethod
|
||||
def htmlParse(html):
|
||||
if not html: return []
|
||||
text = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.S|re.I)
|
||||
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.S|re.I)
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
return [t.strip() for t in text.split(' ') if t.strip()]
|
||||
|
||||
|
||||
class ParameterHandler:
|
||||
def __init__(self, url=''):
|
||||
self._params = {}
|
||||
if url and '?' in url:
|
||||
_, qs = url.split('?', 1)
|
||||
for part in qs.split('&'):
|
||||
if '=' in part:
|
||||
k, v = part.split('=', 1)
|
||||
self._params[k] = v
|
||||
|
||||
def getValue(self, key='', default=''):
|
||||
return self._params.get(key, default)
|
||||
|
||||
def setParam(self, key, value):
|
||||
self._params[str(key)] = str(value)
|
||||
|
||||
def getAll(self):
|
||||
return self._params
|
||||
@@ -0,0 +1 @@
|
||||
/home/matthiasberner/Schreibtisch/kodi/xstreamAddon/plugin.video.xstream/resources/lib/handler/pluginHandler.py
|
||||
@@ -0,0 +1,495 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Python 3
|
||||
|
||||
import time
|
||||
import xbmcgui
|
||||
import re
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
import traceback
|
||||
import ssl
|
||||
import certifi
|
||||
import socket
|
||||
import zlib
|
||||
|
||||
from resources.lib.config import cConfig
|
||||
from resources.lib.logger import logger
|
||||
from resources.lib.cache import cCache
|
||||
from resources.lib.tools import infoDialog
|
||||
from xbmcvfs import translatePath
|
||||
|
||||
from urllib.parse import quote, urlencode, urlparse, quote_plus
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import HTTPHandler, HTTPSHandler, Request, HTTPCookieProcessor, build_opener, urlopen, HTTPRedirectHandler
|
||||
from http.cookiejar import LWPCookieJar, Cookie
|
||||
from http.client import HTTPException
|
||||
from random import choice
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def _doh_resolution(hostname, ip):
|
||||
"""DNS-Bypass via getaddrinfo-Patch: loest 'hostname' temporaer auf 'ip' auf.
|
||||
Anders als ein direkter IP-Connect bleibt das SSL-Zertifikat gueltig, weil die
|
||||
Verbindung weiterhin den echten Hostnamen kennt (SNI + Cert-Hostname stimmen).
|
||||
Patch ist eng gekapselt und wird im finally IMMER zurueckgesetzt."""
|
||||
if not ip:
|
||||
yield
|
||||
return
|
||||
_orig_getaddrinfo = socket.getaddrinfo
|
||||
def _patched(host, *args, **kwargs):
|
||||
if host == hostname:
|
||||
return _orig_getaddrinfo(ip, *args, **kwargs)
|
||||
return _orig_getaddrinfo(host, *args, **kwargs)
|
||||
socket.getaddrinfo = _patched
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
socket.getaddrinfo = _orig_getaddrinfo
|
||||
|
||||
|
||||
class RedirectFilter(HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, hdrs, newurl):
|
||||
if cConfig().getSetting('bypassDNSlock', 'false') != 'true':
|
||||
if 'notice.cuii' in newurl:
|
||||
xbmcgui.Dialog().ok(cConfig().getLocalizedString(30265), cConfig().getLocalizedString(30260) + '\n' + cConfig().getLocalizedString(30261))
|
||||
return None
|
||||
return HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, hdrs, newurl)
|
||||
|
||||
class cRequestHandler:
|
||||
# useful for e.g. tmdb request where multiple requests are made within a loop
|
||||
persistent_openers = {}
|
||||
|
||||
@staticmethod
|
||||
def RandomUA():
|
||||
# Random User Agents aktualisiert 01.07.2026 (Chrome 150 Stable ab 30.06.2026)
|
||||
FF_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0'
|
||||
OPERA_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 OPR/133.0.0.0'
|
||||
EDGE_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0'
|
||||
CHROME_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36'
|
||||
SAFARI_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15'
|
||||
|
||||
_User_Agents = [FF_USER_AGENT, OPERA_USER_AGENT, EDGE_USER_AGENT, CHROME_USER_AGENT, SAFARI_USER_AGENT]
|
||||
return choice(_User_Agents)
|
||||
|
||||
def __init__(self, sUrl, caching=True, ignoreErrors=False, method='GET', data=None, compression=True, jspost=False, ssl_verify=False):
|
||||
self._sUrl = self.__cleanupUrl(sUrl)
|
||||
self._sRealUrl = ''
|
||||
self._USER_AGENT = self.RandomUA()
|
||||
self._aParameters = {}
|
||||
self._headerEntries = {}
|
||||
self._profilePath = translatePath(cConfig().getAddonInfo('profile'))
|
||||
self._cachePath = ''
|
||||
self._cookiePath = ''
|
||||
self._Status = ''
|
||||
self._sResponseHeader = ''
|
||||
self._ssl_verify = ssl_verify
|
||||
self.ignoreDiscard(False)
|
||||
self.ignoreExpired(False)
|
||||
self.caching = caching
|
||||
self.method = method
|
||||
self.data = data
|
||||
self.ignoreErrors = ignoreErrors
|
||||
self.compression = compression
|
||||
self.jspost = jspost
|
||||
self.cacheTime = int(cConfig().getSetting('cacheTime', 1)) * 3600 # Stunden * 3600 = Sekunden
|
||||
self.requestTimeout = int(cConfig().getSetting('requestTimeout', 10))
|
||||
self.bypassDNSlock = (cConfig().getSetting('bypassDNSlock', 'false') == 'true')
|
||||
self.removeBreakLines(True)
|
||||
self.removeNewLines(True)
|
||||
self.__setDefaultHeader()
|
||||
self.__setCachePath()
|
||||
self.__setCookiePath()
|
||||
self.isMemoryCacheActive = (cConfig().getSetting('volatileHtmlCache', 'false') == 'true')
|
||||
if self.isMemoryCacheActive:
|
||||
self._memCache = cCache()
|
||||
|
||||
socket.setdefaulttimeout(self.requestTimeout)
|
||||
|
||||
def getStatus(self):
|
||||
return self._Status
|
||||
|
||||
def removeNewLines(self, bRemoveNewLines):
|
||||
self.__bRemoveNewLines = bRemoveNewLines
|
||||
|
||||
def removeBreakLines(self, bRemoveBreakLines):
|
||||
self.__bRemoveBreakLines = bRemoveBreakLines
|
||||
|
||||
def addHeaderEntry(self, sHeaderKey, sHeaderValue):
|
||||
self._headerEntries[sHeaderKey] = sHeaderValue
|
||||
|
||||
def getHeaderEntry(self, sHeaderKey):
|
||||
if sHeaderKey in self._headerEntries:
|
||||
return self._headerEntries[sHeaderKey]
|
||||
|
||||
def addParameters(self, key, value, Quote=False):
|
||||
self._aParameters[key] = value if not Quote else quote(str(value))
|
||||
|
||||
def getResponseHeader(self):
|
||||
return self._sResponseHeader
|
||||
|
||||
def getRealUrl(self):
|
||||
return self._sRealUrl
|
||||
|
||||
def getRequestUri(self):
|
||||
return self._sUrl + '?' + urlencode(self._aParameters)
|
||||
|
||||
def __setDefaultHeader(self):
|
||||
self.addHeaderEntry('User-Agent', self._USER_AGENT)
|
||||
self.addHeaderEntry('Accept-Language', 'de,en-US;q=0.7,en;q=0.3')
|
||||
self.addHeaderEntry('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8')
|
||||
if self.compression:
|
||||
self.addHeaderEntry('Accept-Encoding', 'gzip, deflate')
|
||||
self.addHeaderEntry('Connection', 'keep-alive')
|
||||
self.addHeaderEntry('Keep-Alive', 'timeout=5')
|
||||
|
||||
@staticmethod
|
||||
def __getDefaultHandler(ssl_verify):
|
||||
if ssl_verify:
|
||||
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
ssl_context.check_hostname = True
|
||||
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
||||
return [HTTPSHandler(context=ssl_context)]
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
return [HTTPSHandler(context=ssl_context)]
|
||||
|
||||
@staticmethod
|
||||
def __cleanupUrl(url):
|
||||
if not isinstance(url, str):
|
||||
return ''
|
||||
if not isinstance(url, str):
|
||||
return ''
|
||||
#p = urlparse(url)
|
||||
#if p.query:
|
||||
# query = quote_plus(p.query).replace('%3D', '=').replace('%26', '&')
|
||||
# p = p._replace(query=p.query.replace(p.query, query))
|
||||
#else:
|
||||
# path = quote_plus(p.path).replace('%2F', '/').replace('%26', '&').replace('%3D', '=')
|
||||
# p = p._replace(path=p.path.replace(p.path, path))
|
||||
#return p.geturl()
|
||||
return url
|
||||
|
||||
def request(self):
|
||||
if self.caching and self.cacheTime > 0 and self.method == 'GET' and self.data is None:
|
||||
if self.isMemoryCacheActive:
|
||||
sContent = self.__readVolatileCache(self.getRequestUri(), self.cacheTime)
|
||||
else:
|
||||
sContent = self.__readPersistentCache(self.getRequestUri())
|
||||
if sContent:
|
||||
self._Status = '200'
|
||||
return sContent
|
||||
else:
|
||||
logger.info('-> [requestHandler]: read html for %s' % self.getRequestUri())
|
||||
|
||||
# DNS-Bypass global ueber Setting (kein per-Site-Flag mehr)
|
||||
if self.bypassDNSlock:
|
||||
ip_override = self.__doh_request(self._sUrl)
|
||||
else:
|
||||
ip_override = None
|
||||
_doh_host = urlparse(self._sUrl).hostname
|
||||
|
||||
cookieJar = LWPCookieJar(filename=self._cookiePath)
|
||||
try:
|
||||
cookieJar.load(ignore_discard=self.__bIgnoreDiscard, ignore_expires=self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
|
||||
domain = urlparse(self._sUrl).netloc
|
||||
if domain in cRequestHandler.persistent_openers:
|
||||
opener = cRequestHandler.persistent_openers[domain]
|
||||
else:
|
||||
handlers = self.__getDefaultHandler(self._ssl_verify)
|
||||
handlers += [HTTPHandler(), HTTPCookieProcessor(cookiejar=cookieJar), RedirectFilter()]
|
||||
opener = build_opener(*handlers)
|
||||
cRequestHandler.persistent_openers[domain] = opener
|
||||
|
||||
# Prepare parameters for GET/POST
|
||||
if self.method == 'POST':
|
||||
if self.data is not None:
|
||||
if isinstance(self.data, dict):
|
||||
# Default: form data
|
||||
sParameters = urlencode(self.data).encode()
|
||||
elif isinstance(self.data, str):
|
||||
sParameters = self.data.encode()
|
||||
else:
|
||||
sParameters = self.data
|
||||
else:
|
||||
sParameters = None
|
||||
else:
|
||||
sParameters = json.dumps(self._aParameters).encode() if self.jspost else urlencode(self._aParameters, True).encode()
|
||||
if len(sParameters) == 0:
|
||||
sParameters = None
|
||||
|
||||
oRequest = Request(self._sUrl, sParameters if sParameters and len(sParameters) > 0 else None)
|
||||
|
||||
for key, value in self._headerEntries.items():
|
||||
oRequest.add_header(key, value)
|
||||
|
||||
if self.method == 'POST' and 'Content-Type' not in self._headerEntries:
|
||||
oRequest.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
||||
elif self.jspost:
|
||||
oRequest.add_header('Content-Type', 'application/json')
|
||||
|
||||
cookieJar.add_cookie_header(oRequest)
|
||||
|
||||
try:
|
||||
with _doh_resolution(_doh_host, ip_override):
|
||||
oResponse = opener.open(oRequest)
|
||||
except HTTPError as e:
|
||||
if e.code >= 400:
|
||||
self._Status = str(e.code)
|
||||
data = e.fp.read()
|
||||
if 'DDOS-GUARD' in str(data):
|
||||
opener = build_opener(HTTPCookieProcessor(cookieJar))
|
||||
opener.addheaders = [('User-agent', self._USER_AGENT), ('Referer', self._sUrl)]
|
||||
response = opener.open('https://check.ddos-guard.net/check.js')
|
||||
content = response.read().decode('utf-8', 'replace')
|
||||
url2 = re.findall("Image.*?'([^']+)'; new", content)
|
||||
url3 = urlparse(self._sUrl)
|
||||
url3 = '%s://%s/%s' % (url3.scheme, url3.netloc, url2[0])
|
||||
opener = build_opener(HTTPCookieProcessor(cookieJar))
|
||||
opener.addheaders = [('User-agent', self._USER_AGENT), ('Referer', self._sUrl)]
|
||||
opener.open(url3).read()
|
||||
opener = build_opener(HTTPCookieProcessor(cookieJar))
|
||||
opener.addheaders = [('User-agent', self._USER_AGENT), ('Referer', self._sUrl)]
|
||||
oResponse = opener.open(self._sUrl, sParameters if len(sParameters) > 0 else None)
|
||||
if not oResponse:
|
||||
logger.error(' -> [requestHandler]: Failed DDOS-GUARD active: ' + self._sUrl)
|
||||
return 'DDOS GUARD SCHUTZ'
|
||||
elif 'cloudflare' in str(e.headers):
|
||||
if not self.ignoreErrors:
|
||||
infoDialog(cConfig().getLocalizedString(30829), icon='WARNING', time=10000)
|
||||
logger.error(' -> [requestHandler]: Failed Cloudflare active: ' + self._sUrl)
|
||||
return 'CLOUDFLARE-SCHUTZ AKTIV' # Meldung geht als "e.doc" in die exception nach default.py
|
||||
else:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', cConfig().getLocalizedString(30259) + ' {0} {1}'.format(self._sUrl, str(e)))
|
||||
logger.error(' -> [requestHandler]: HTTPError ' + str(e) + ' Url: ' + self._sUrl)
|
||||
return 'SEITE NICHT ERREICHBAR'
|
||||
else:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', cConfig().getLocalizedString(30259) + ' {0} {1}'.format(self._sUrl, str(e)))
|
||||
logger.error(' -> [requestHandler]: HTTPError ' + str(e) + ' Url: ' + self._sUrl)
|
||||
return 'SEITE NICHT ERREICHBAR'
|
||||
except URLError as e:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', str(e.reason))
|
||||
logger.error(' -> [requestHandler]: URLError ' + str(e.reason) + ' Url: ' + self._sUrl)
|
||||
return 'URL FEHLER'
|
||||
except HTTPException as e:
|
||||
if not self.ignoreErrors:
|
||||
xbmcgui.Dialog().ok('xStream', str(e))
|
||||
logger.error(' -> [requestHandler]: HTTPException ' + str(e) + ' Url: ' + self._sUrl)
|
||||
return 'TIMEOUT'
|
||||
|
||||
self._sResponseHeader = oResponse.info()
|
||||
|
||||
content_encoding = self._sResponseHeader.get('Content-Encoding', '').lower()
|
||||
if content_encoding:
|
||||
raw_content = oResponse.read()
|
||||
if content_encoding == 'gzip':
|
||||
decompressed = zlib.decompress(raw_content, wbits=zlib.MAX_WBITS | 16)
|
||||
elif content_encoding == 'deflate':
|
||||
decompressed = zlib.decompress(raw_content, wbits=-zlib.MAX_WBITS)
|
||||
else:
|
||||
decompressed = raw_content
|
||||
sContent = decompressed.decode('utf-8', 'replace')
|
||||
else:
|
||||
sContent = oResponse.read().decode('utf-8', 'replace')
|
||||
|
||||
if 'lazingfast' in sContent:
|
||||
bf = cBF().resolve(self._sUrl, sContent, cookieJar, self._USER_AGENT, sParameters)
|
||||
if bf:
|
||||
sContent = bf
|
||||
else:
|
||||
logger.error(' -> [requestHandler]: Failed Blazingfast active: ' + self._sUrl)
|
||||
|
||||
try:
|
||||
cookieJar.save(ignore_discard=self.__bIgnoreDiscard, ignore_expires=self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.error(' -> [requestHandler]: Failed save cookie: %s' % e)
|
||||
|
||||
self._sRealUrl = oResponse.geturl()
|
||||
self._Status = oResponse.getcode() if self._sUrl == self._sRealUrl else '301'
|
||||
|
||||
if self.__bRemoveNewLines:
|
||||
sContent = sContent.replace('\n', '').replace('\r\t', '')
|
||||
if self.__bRemoveBreakLines:
|
||||
sContent = sContent.replace(' ', '')
|
||||
|
||||
if self.caching and self.cacheTime > 0 and self.method == 'GET' and self.data is None:
|
||||
if self.isMemoryCacheActive:
|
||||
self.__writeVolatileCache(self.getRequestUri(), sContent)
|
||||
else:
|
||||
self.__writePersistentCache(self.getRequestUri(), sContent)
|
||||
|
||||
return sContent
|
||||
|
||||
def __setCookiePath(self):
|
||||
cookieFile = os.path.join(self._profilePath, 'cookies')
|
||||
if not os.path.exists(cookieFile):
|
||||
os.makedirs(cookieFile)
|
||||
if 'dummy' not in self._sUrl:
|
||||
cookieFile = os.path.join(cookieFile, urlparse(self._sUrl).netloc.replace('.', '_') + '.txt')
|
||||
if not os.path.exists(cookieFile):
|
||||
open(cookieFile, 'w').close()
|
||||
self._cookiePath = cookieFile
|
||||
|
||||
def getCookie(self, sCookieName, sDomain=''):
|
||||
cookieJar = LWPCookieJar()
|
||||
try:
|
||||
cookieJar.load(self._cookiePath, self.__bIgnoreDiscard, self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
for entry in cookieJar:
|
||||
if entry.name == sCookieName:
|
||||
if sDomain == '':
|
||||
return entry
|
||||
elif entry.domain == sDomain:
|
||||
return entry
|
||||
return False
|
||||
|
||||
def setCookie(self, oCookie):
|
||||
cookieJar = LWPCookieJar()
|
||||
try:
|
||||
cookieJar.load(self._cookiePath, self.__bIgnoreDiscard, self.__bIgnoreExpired)
|
||||
cookieJar.set_cookie(oCookie)
|
||||
cookieJar.save(self._cookiePath, self.__bIgnoreDiscard, self.__bIgnoreExpired)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
def ignoreDiscard(self, bIgnoreDiscard):
|
||||
self.__bIgnoreDiscard = bIgnoreDiscard
|
||||
|
||||
def ignoreExpired(self, bIgnoreExpired):
|
||||
self.__bIgnoreExpired = bIgnoreExpired
|
||||
|
||||
def __doh_request(self, url, doh_server="https://cloudflare-dns.com/dns-query"):
|
||||
# Parse the URL
|
||||
parsed_url = urlparse(url)
|
||||
hostname = parsed_url.hostname
|
||||
# Bewusst NICHT gecacht: DDoS-Guard-Edges rotieren, eine im RAM-Cache
|
||||
# festgehaltene IP koennte innerhalb der TTL sterben -> immer frisch aufloesen.
|
||||
params = urlencode({"name": hostname, "type": "A"})
|
||||
doh_url = f"{doh_server}?{params}"
|
||||
req = Request(doh_url)
|
||||
req.add_header("Accept", "application/dns-json")
|
||||
|
||||
try:
|
||||
response = urlopen(req, timeout=5)
|
||||
response_text = response.read().decode("utf-8", "replace")
|
||||
dns_response = json.loads(response_text)
|
||||
if "Answer" not in dns_response:
|
||||
raise Exception("Invalid DNS response")
|
||||
# Ersten echten A-Record (type 1) aus der Answer-Liste nehmen statt stur [0]:
|
||||
# bei CNAME-Ketten kann die IP erst weiter hinten stehen, [0] waere dann der CNAME.
|
||||
ip_address = next((a["data"] for a in dns_response["Answer"] if a.get("type") == 1), None)
|
||||
if not ip_address:
|
||||
raise Exception("No A record in DNS answer")
|
||||
return ip_address
|
||||
except Exception as e:
|
||||
logger.error(' -> [requestHandler]: DNS query failed: %s' % e)
|
||||
return None
|
||||
|
||||
def __setCachePath(self):
|
||||
cache = os.path.join(self._profilePath, 'htmlcache')
|
||||
if not os.path.exists(cache):
|
||||
os.makedirs(cache)
|
||||
self._cachePath = cache
|
||||
|
||||
def __readPersistentCache(self, url):
|
||||
h = hashlib.md5(url.encode('utf8')).hexdigest()
|
||||
cacheFile = os.path.join(self._cachePath, h)
|
||||
fileAge = self.getFileAge(cacheFile)
|
||||
if 0 < fileAge < self.cacheTime:
|
||||
try:
|
||||
with open(cacheFile, 'rb') as f:
|
||||
content = f.read().decode('utf8')
|
||||
except Exception:
|
||||
logger.error(' -> [requestHandler]: Could not read Cache')
|
||||
if content:
|
||||
logger.info(' -> [requestHandler]: read html for %s from cache' % url)
|
||||
return content
|
||||
return None
|
||||
|
||||
def __writePersistentCache(self, url, content):
|
||||
try:
|
||||
h = hashlib.md5(url.encode('utf8')).hexdigest()
|
||||
with open(os.path.join(self._cachePath, h), 'wb') as f:
|
||||
f.write(content.encode('utf8'))
|
||||
except Exception:
|
||||
logger.error(' -> [requestHandler]: Could not write Cache')
|
||||
|
||||
def __writeVolatileCache(self, url, content):
|
||||
self._memCache.set(hashlib.md5(url.encode('utf8')).hexdigest(), content)
|
||||
|
||||
def __readVolatileCache(self, url, cache_time):
|
||||
entry = self._memCache.get(hashlib.md5(url.encode('utf8')).hexdigest(), cache_time)
|
||||
if entry:
|
||||
logger.info('-> [requestHandler]: read html for %s from cache' % url)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def getFileAge(cacheFile):
|
||||
try:
|
||||
return time.time() - os.stat(cacheFile).st_mtime
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def clearCache(self, silent=False):
|
||||
# clear volatile cache
|
||||
if self.isMemoryCacheActive:
|
||||
self._memCache.clear()
|
||||
cRequestHandler.persistent_openers.clear()
|
||||
|
||||
# clear persistent cache
|
||||
files = os.listdir(self._cachePath)
|
||||
for file in files:
|
||||
os.remove(os.path.join(self._cachePath, file))
|
||||
if not silent and files:
|
||||
infoDialog(cConfig().getLocalizedString(30405), icon='INFO')
|
||||
|
||||
|
||||
class cBF:
|
||||
def resolve(self, url, html, cookie_jar, user_agent, sParameters):
|
||||
page = urlparse(url).scheme + '://' + urlparse(url).netloc
|
||||
j = re.compile('<script[^>]src="([^"]+)').findall(html)
|
||||
if j:
|
||||
opener = build_opener(HTTPCookieProcessor(cookie_jar))
|
||||
opener.addheaders = [('User-agent', user_agent), ('Referer', url)]
|
||||
opener.open(page + j[0])
|
||||
a = re.compile(r'xhr\.open\("GET","([^,]+)",').findall(html)
|
||||
if a:
|
||||
import random
|
||||
aespage = page + a[0].replace('" + ww +"', str(random.randint(700, 1500)))
|
||||
opener = build_opener(HTTPCookieProcessor(cookie_jar))
|
||||
opener.addheaders = [('User-agent', user_agent), ('Referer', url)]
|
||||
html = opener.open(aespage).read().decode('utf-8', 'replace')
|
||||
cval = self.aes_decode(html)
|
||||
cdata = re.compile('cookie="([^="]+).*?domain[^>]=([^;]+)').findall(html)
|
||||
if cval and cdata:
|
||||
c = Cookie(version=0, name=cdata[0][0], value=cval, port=None, port_specified=False, domain=cdata[0][1], domain_specified=True, domain_initial_dot=False, path="/", path_specified=True, secure=False, expires=time.time() + 21600, discard=False, comment=None, comment_url=None, rest={})
|
||||
cookie_jar.set_cookie(c)
|
||||
opener = build_opener(HTTPCookieProcessor(cookie_jar))
|
||||
opener.addheaders = [('User-agent', user_agent), ('Referer', url)]
|
||||
return opener.open(url, sParameters if len(sParameters) > 0 else None).read().decode('utf-8', 'replace')
|
||||
|
||||
@staticmethod
|
||||
def aes_decode(html):
|
||||
try:
|
||||
import pyaes
|
||||
keys = re.compile(r'toNumbers\("([^"]+)"').findall(html)
|
||||
if keys:
|
||||
from binascii import hexlify, unhexlify
|
||||
msg = unhexlify(keys[2])
|
||||
key = unhexlify(keys[0])
|
||||
iv = unhexlify(keys[1])
|
||||
decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv))
|
||||
plain_text = decrypter.feed(msg)
|
||||
plain_text += decrypter.feed()
|
||||
return hexlify(plain_text).decode()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
Reference in New Issue
Block a user