source: titan/mediathek/localhoster/lib/net.py @ 42561

Last change on this file since 42561 was 42561, checked in by obi, 16 months ago

fix net py weneed cloudflare

File size: 24.8 KB
Line 
1'''
2    common XBMC Module
3    Copyright (C) 2011 t0mm0
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14
15    You should have received a copy of the GNU General Public License
16    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17'''
18
19import random
20import cookielib
21import gzip
22import re
23import StringIO
24import urllib
25import urllib2
26import socket
27from urlparse import urlparse
28from urlparse import urlunparse
29import time
30
31BR_VERS = [
32    ['%s.0' % i for i in xrange(18, 50)],
33    ['37.0.2062.103', '37.0.2062.120', '37.0.2062.124', '38.0.2125.101', '38.0.2125.104', '38.0.2125.111', '39.0.2171.71', '39.0.2171.95', '39.0.2171.99', '40.0.2214.93', '40.0.2214.111',
34     '40.0.2214.115', '42.0.2311.90', '42.0.2311.135', '42.0.2311.152', '43.0.2357.81', '43.0.2357.124', '44.0.2403.155', '44.0.2403.157', '45.0.2454.101', '45.0.2454.85', '46.0.2490.71',
35     '46.0.2490.80', '46.0.2490.86', '47.0.2526.73', '47.0.2526.80', '48.0.2564.116', '49.0.2623.112', '50.0.2661.86'],
36    ['11.0'],
37    ['8.0', '9.0', '10.0', '10.6']]
38WIN_VERS = ['Windows NT 10.0', 'Windows NT 7.0', 'Windows NT 6.3', 'Windows NT 6.2', 'Windows NT 6.1', 'Windows NT 6.0', 'Windows NT 5.1', 'Windows NT 5.0']
39FEATURES = ['; WOW64', '; Win64; IA64', '; Win64; x64', '']
40RAND_UAS = ['Mozilla/5.0 ({win_ver}{feature}; rv:{br_ver}) Gecko/20100101 Firefox/{br_ver}',
41            'Mozilla/5.0 ({win_ver}{feature}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{br_ver} Safari/537.36',
42            'Mozilla/5.0 ({win_ver}{feature}; Trident/7.0; rv:{br_ver}) like Gecko',
43            'Mozilla/5.0 (compatible; MSIE {br_ver}; {win_ver}{feature}; Trident/6.0)']
44def get_ua():
45#    try: last_gen = int(kodi.get_setting('last_ua_create'))
46    try: last_gen = 0
47    except: last_gen = 0
48#    if not kodi.get_setting('current_ua') or last_gen < (time.time() - (7 * 24 * 60 * 60)):
49    index = random.randrange(len(RAND_UAS))
50    versions = {'win_ver': random.choice(WIN_VERS), 'feature': random.choice(FEATURES), 'br_ver': random.choice(BR_VERS[index])}
51    user_agent = RAND_UAS[index].format(**versions)
52        # logger.log('Creating New User Agent: %s' % (user_agent), log_utils.LOGDEBUG)
53#        kodi.set_setting('current_ua', user_agent)
54#        kodi.set_setting('last_ua_create', str(int(time.time())))
55#    else:
56#        user_agent = kodi.get_setting('current_ua')
57    return user_agent
58
59class HeadRequest(urllib2.Request):
60    '''A Request class that sends HEAD requests'''
61    def get_method(self):
62        return 'HEAD'
63
64class Net:
65    '''
66    This class wraps :mod:`urllib2` and provides an easy way to make http
67    requests while taking care of cookies, proxies, gzip compression and
68    character encoding.
69   
70    Example::
71   
72        from addon.common.net import Net
73        net = Net()
74        response = net.http_GET('http://xbmc.org')
75        print response.content
76    '''
77    IE_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko'
78    FF_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0'
79    IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25'
80    ANDROID_USER_AGENT = 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36'
81
82    _cj = cookielib.MozillaCookieJar()
83
84    _proxy = None
85    _user_agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36'
86    _accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
87    _http_debug = False
88    _socket_timeout = 60
89
90    def __init__(self, cookie_file='', proxy='', user_agent='', 
91                 http_debug=False, accept=_accept, socket_timeout=_socket_timeout, cloudflare=False):
92        '''
93        Kwargs:
94            cookie_file (str): Full path to a file to be used to load and save
95            cookies to.
96           
97            proxy (str): Proxy setting (eg.
98            ``'http://user:pass@example.com:1234'``)
99           
100            user_agent (str): String to use as the User Agent header. If not
101            supplied the class will use a default user agent (chrome)
102           
103            http_debug (bool): Set ``True`` to have HTTP header info written to
104            the XBMC log for all requests.
105           
106            accept (str) : String to use as HTTP Request Accept header.
107           
108            socket_timeout (int): time in seconds for socket connections to wait until time out
109
110            cloudflare (bool): Set ``True`` to check all requests that raise HTTPError 503 for Cloudflare challenge and solve
111            This can be changed per request as well, see http_GET, http_PUSH
112        '''
113   
114        #Set socket timeout - Useful for slow connections
115        socket.setdefaulttimeout(socket_timeout)
116
117        # empty jar for each instance rather than scope of the import
118        self._cloudflare_jar = cookielib.MozillaCookieJar()
119
120        self.cloudflare = cloudflare
121        if cookie_file:
122            self.set_cookies(cookie_file)
123        if proxy:
124            self.set_proxy(proxy)
125        if user_agent:
126            self.set_user_agent(user_agent)
127        self._http_debug = http_debug
128        self._update_opener()
129       
130   
131    def set_cookies(self, cookie_file):
132        '''
133        Set the cookie file and try to load cookies from it if it exists.
134       
135        Args:
136            cookie_file (str): Full path to a file to be used to load and save
137            cookies to.
138        '''
139        try:
140            self._cj.load(cookie_file, ignore_discard=True)
141            self._update_opener()
142            return True
143        except:
144            return False
145       
146   
147    def get_cookies(self):
148        '''Returns A dictionary containing all cookie information by domain.'''
149        return self._cj._cookies
150
151
152    def save_cookies(self, cookie_file):
153        '''
154        Saves cookies to a file.
155       
156        Args:
157            cookie_file (str): Full path to a file to save cookies to.
158        '''
159        self._cj.save(cookie_file, ignore_discard=True)       
160
161       
162    def set_proxy(self, proxy):
163        '''
164        Args:
165            proxy (str): Proxy setting (eg.
166            ``'http://user:pass@example.com:1234'``)
167        '''
168        self._proxy = proxy
169        self._update_opener()
170
171       
172    def get_proxy(self):
173        '''Returns string containing proxy details.'''
174        return self._proxy
175       
176       
177    def set_user_agent(self, user_agent):
178        '''
179        Args:
180            user_agent (str): String to use as the User Agent header.
181        '''
182        self._user_agent = user_agent
183
184       
185    def get_user_agent(self):
186        '''Returns user agent string.'''
187        return self._user_agent
188
189
190    def _update_opener(self, cloudflare_jar=False):
191        """
192        Builds and installs a new opener to be used by all future calls to
193        :func:`urllib2.urlopen`.
194        """
195        if self._http_debug:
196            http = urllib2.HTTPHandler(debuglevel=1)
197        else:
198            http = urllib2.HTTPHandler()
199
200        if cloudflare_jar:
201            self._cloudflare_jar = cookielib.MozillaCookieJar()
202            jar = self._cloudflare_jar
203        else:
204            jar = self._cj
205
206        if self._proxy:
207            opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(jar),
208                                          urllib2.ProxyHandler({'http':
209                                                                self._proxy}),
210                                          urllib2.HTTPBasicAuthHandler(),
211                                          http)
212
213        else:
214            opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(jar),
215                                          urllib2.HTTPBasicAuthHandler(),
216                                          http)
217        urllib2.install_opener(opener)
218
219
220    def _parseJSString(self, s):
221        """
222        lambda
223        plugin.video.genesis\resources\lib\libraries\cloudflare.py
224        https://offshoregit.com/lambda81/
225        """
226        try:
227            offset=1 if s[0]=='+' else 0
228            val = int(eval(s.replace('!+[]','1').replace('!![]','1').replace('[]','0').replace('(','str(')[offset:]))
229            return val
230        except:
231            raise Exception
232
233
234    def _cloudflare_challenge(self, url, challenge, form_data={}, headers={}, compression=True):
235        """
236        Use _set_cloudflare to call this, not intended to be called directly.
237        Solve challenge and make request with cloudflare cookie jar
238
239        Part from:
240        lambda
241        plugin.video.genesis\resources\lib\libraries\cloudflare.py
242        https://offshoregit.com/lambda81/
243        """
244        jschl = re.compile('name="jschl_vc" value="(.+?)"/>').findall(challenge)[0]
245        init = re.compile('setTimeout\(function\(\){\s*.*?.*:(.*?)};').findall(challenge)[0]
246        builder = re.compile(r"challenge-form\'\);\s*(.*)a.v").findall(challenge)[0]
247        decrypt_val = self._parseJSString(init)
248        lines = builder.split(';')
249
250        for line in lines:
251            if len(line)>0 and '=' in line:
252                sections=line.split('=')
253                line_val = self._parseJSString(sections[1])
254                decrypt_val = int(eval(str(decrypt_val)+sections[0][-1]+str(line_val)))
255
256        path = urlparse(url).path
257        netloc = urlparse(url).netloc
258        if not netloc:
259            netloc = path
260
261        answer = decrypt_val + len(netloc)
262
263        url = url.rstrip('/')
264        query = '%s/cdn-cgi/l/chk_jschl?jschl_vc=%s&jschl_answer=%s' % (url, jschl, answer)
265
266        if 'type="hidden" name="pass"' in challenge:
267            passval = re.compile('name="pass" value="(.*?)"').findall(challenge)[0]
268            query = '%s/cdn-cgi/l/chk_jschl?pass=%s&jschl_vc=%s&jschl_answer=%s' % \
269                    (url, urllib.quote_plus(passval), jschl, answer)
270            time.sleep(9)
271
272        self._update_opener(cloudflare_jar=True)
273        req = urllib2.Request(query)
274        if form_data:
275            form_data = urllib.urlencode(form_data)
276            req = urllib2.Request(query, form_data)
277        req.add_header('User-Agent', self._user_agent)
278        for k, v in headers.items():
279            req.add_header(k, v)
280        if compression:
281            req.add_header('Accept-Encoding', 'gzip')
282        try:
283            response = urllib2.urlopen(req)
284        except urllib2.HTTPError as e:
285            pass
286
287
288    def _set_cloudflare(self, url, challenge, form_data={}, headers={}, compression=True):
289        """
290        Entry Point for _cloudflare_challenge
291        Calls cloudflare_challenge on netloc, not full url w/ path
292        Puts any cloudflare cookies in the main cookie jar
293        Args:
294            url (str): The URL to site of potential Cloudflare IUA.
295
296            challenge (str): html contents of the page that raised 503, containing potential Cloudflare IUA Challenge
297        Kwargs:
298            form_data (dict): A dictionary of form data if pass-through from POST.
299
300            headers (dict): A dictionary describing any headers you would like
301            to add to the request. (eg. ``{'X-Test': 'testing'}``)
302
303            compression (bool): If ``True`` (default), try to use gzip
304            compression.
305        """
306        netloc = urlparse(url).netloc
307        if not netloc:
308            netloc = urlparse(url).path
309        cloudflare_url = urlunparse((urlparse(url).scheme, netloc, '', '', '', ''))
310        try:
311            self._cloudflare_challenge(cloudflare_url, challenge, form_data, headers, compression)
312            for c in self._cloudflare_jar:
313                self._cj.set_cookie(c)
314            self._update_opener()
315        except:
316            # make sure we update to main jar
317            self._update_opener()
318            raise Exception
319
320
321    def url_with_headers(self, url, referer=None, user_agent=None, cookies=None, proxy=None, connection_timeout=None,
322                         encoding='', accept_charset='', sslcipherlist='', noshout='false', seekable='1'):
323        '''
324        Return url with Referer, User-Agent, Cookies, Proxy, Connection-Timeout, Encoding, Accept-Charset,
325        SSLCipherList, NoShout and Seekable
326        Based on: https://github.com/xbmc/xbmc/blob/master/xbmc/filesystem/CurlFile.cpp#L782
327        Args:
328            url (str): The URL to append headers to.
329
330        Kwargs:
331            referer (str): If None (default), urlunparse((urlparse(url).scheme, netloc, path, '', '', '')) is used and append if set
332
333            user_agent (str): If None (default), self._user_agent is used and append if set
334
335            cookies (bool): If ``None`` (default), use self.cloudflare as bool (False as default)
336            Append cookies to URL as well
337
338            proxy (str): If None (default), self.proxy is used and append if set
339
340            connection_timeout (str): If None (default), self._socket_timeout is used and append if set
341
342            encoding (str): append if set
343
344            accept_charset (str): append if set
345
346            sslcipherlist (str): append if set
347
348            noshout (str): 'true'/'false', skip shout, append if 'true' ('false' is kodi default)
349
350            seekable (str): '0'/'1', append if 0 ('1' is kodi default)
351        Returns:
352            http://example.com/myimage.png|Referer=%%%%%&User-Agent=%%%%%...
353        '''
354        kodi_schemes = ('special', 'plugin', 'script', 'profile')
355        if ('://' not in url) or (url.startswith(kodi_schemes)):
356            # don't waste time and return url
357            return url
358
359        _tmp = re.search('(.+?)(?:\|.*|$)', url)
360        if _tmp:
361            # trim any headers that may already be attached to url
362            url = _tmp.group(1)
363
364        if referer is not None:
365            try:
366                referer = str(referer)
367            except:
368                referer = None
369        if referer is None:
370            path = urlparse(url).path
371            netloc = urlparse(url).netloc
372            if not netloc:
373                netloc = path
374                path = ''
375            referer = urlunparse((urlparse(url).scheme, netloc, path, '', '', ''))
376            if referer == url:
377                index = path.rfind('/')
378                if index >= 0:
379                    referer = urlunparse((urlparse(url).scheme, netloc, path[:index], '', '', ''))
380        if user_agent is None:
381            user_agent = self._user_agent
382        else:
383            try:
384                user_agent = str(user_agent)
385            except:
386                user_agent = self._user_agent
387        if cookies is None:
388            cookies = self.cloudflare
389        if proxy is None:
390            proxy = self._proxy
391        if connection_timeout is None:
392            connection_timeout = self._socket_timeout
393        try:
394            connection_timeout = str(connection_timeout)
395        except:
396            connection_timeout = None
397        try:
398            if str(seekable) != '0':
399                seekable = None
400        except:
401            seekable = None
402        try:
403            if str(noshout).lower() != 'true':
404                noshout = None
405        except:
406            noshout = None
407
408        url += '|Referer=' + urllib.quote_plus(referer) + '&User-Agent=' + urllib.quote_plus(user_agent)
409        if proxy:
410            try:
411                url += '&HTTPProxy=' + urllib.quote_plus(str(proxy))
412            except:
413                pass
414        if connection_timeout:
415            url += '&Connection-Timeout=' + urllib.quote_plus(connection_timeout)
416        if encoding:
417            try:
418                url += '&Encoding=' + urllib.quote_plus(str(encoding))
419            except:
420                pass
421        if accept_charset:
422            try:
423                url += '&Accept-Charset=' + urllib.quote_plus(str(accept_charset))
424            except:
425                pass
426        if sslcipherlist:
427            try:
428                url += '&SSLCipherList=' + urllib.quote_plus(str(sslcipherlist))
429            except:
430                pass
431        if noshout:
432            url += '&NoShout=' + urllib.quote_plus(str(noshout).lower())
433        if seekable:
434            url += '&Seekable=' + urllib.quote_plus(str(seekable))
435        if cookies:
436            cookie_string = ''
437            for c in self._cj:
438                if c.domain and (c.domain.lstrip('.') in url):
439                    cookie_string += '%s=%s;' % (c.name, c.value)
440            if cookie_string:
441                url += '&Cookie=' + urllib.quote_plus(cookie_string)
442        return url
443
444
445    def http_GET(self, url, headers={}, compression=True, cloudflare=None):
446        '''
447        Perform an HTTP GET request.
448       
449        Args:
450            url (str): The URL to GET.
451           
452        Kwargs:
453            headers (dict): A dictionary describing any headers you would like
454            to add to the request. (eg. ``{'X-Test': 'testing'}``)
455
456            compression (bool): If ``True`` (default), try to use gzip
457            compression.
458
459            cloudflare (bool): If ``None`` (default), use self.cloudflare as bool (False as default)
460            On HTTPError 503 check for Cloudflare challenge and solve
461        Returns:
462            An :class:`HttpResponse` object containing headers and other
463            meta-information about the page and the page content.
464        '''
465        if cloudflare is None:
466            cloudflare = self.cloudflare
467        return self._fetch(url, headers=headers, compression=compression, cloudflare=cloudflare)
468       
469
470    def http_POST(self, url, form_data, headers={}, compression=True, cloudflare=None):
471        '''
472        Perform an HTTP POST request.
473       
474        Args:
475            url (str): The URL to POST.
476           
477            form_data (dict): A dictionary of form data to POST.
478           
479        Kwargs:
480            headers (dict): A dictionary describing any headers you would like
481            to add to the request. (eg. ``{'X-Test': 'testing'}``)
482
483            compression (bool): If ``True`` (default), try to use gzip
484            compression.
485
486            cloudflare (bool): If ``None`` (default), use self.cloudflare as bool (False as default)
487            On HTTPError 503 check for Cloudflare challenge and solve
488        Returns:
489            An :class:`HttpResponse` object containing headers and other
490            meta-information about the page and the page content.
491        '''
492        if cloudflare is None:
493            cloudflare = self.cloudflare
494        return self._fetch(url, form_data, headers=headers,
495                           compression=compression, cloudflare=cloudflare)
496
497   
498    def http_HEAD(self, url, headers={}):
499        '''
500        Perform an HTTP HEAD request.
501       
502        Args:
503            url (str): The URL to GET.
504       
505        Kwargs:
506            headers (dict): A dictionary describing any headers you would like
507            to add to the request. (eg. ``{'X-Test': 'testing'}``)
508       
509        Returns:
510            An :class:`HttpResponse` object containing headers and other
511            meta-information about the page.
512        '''
513        req = HeadRequest(url)
514        req.add_header('User-Agent', self._user_agent)
515        req.add_header('Accept', self._accept)
516        for k, v in headers.items():
517            req.add_header(k, v)
518        response = urllib2.urlopen(req)
519        return HttpResponse(response)
520
521
522    def _fetch(self, url, form_data={}, headers={}, compression=True, cloudflare=None):
523        '''
524        Perform an HTTP GET or POST request.
525       
526        Args:
527            url (str): The URL to GET or POST.
528           
529            form_data (dict): A dictionary of form data to POST. If empty, the
530            request will be a GET, if it contains form data it will be a POST.
531           
532        Kwargs:
533            headers (dict): A dictionary describing any headers you would like
534            to add to the request. (eg. ``{'X-Test': 'testing'}``)
535
536            compression (bool): If ``True`` (default), try to use gzip
537            compression.
538
539            cloudflare (bool): If ``None`` (default), use self.cloudflare as bool (False as default)
540            On HTTPError 503 check for Cloudflare challenge and solve
541        Returns:
542            An :class:`HttpResponse` object containing headers and other
543            meta-information about the page and the page content.
544        '''
545        if cloudflare is None:
546            cloudflare = self.cloudflare
547        encoding = ''
548        req = urllib2.Request(url)
549        if form_data:
550            form_data = urllib.urlencode(form_data)
551            req = urllib2.Request(url, form_data)
552        req.add_header('User-Agent', self._user_agent)
553        for k, v in headers.items():
554            req.add_header(k, v)
555        if compression:
556            req.add_header('Accept-Encoding', 'gzip')
557        if not cloudflare:
558            response = urllib2.urlopen(req)
559            return HttpResponse(response)
560        else:
561            try:
562                response = urllib2.urlopen(req)
563                return HttpResponse(response)
564            except urllib2.HTTPError as e:
565                if e.code == 503:
566                    try:
567                        self._set_cloudflare(url, e.read(), form_data, headers, compression)
568                    except:
569                        raise urllib2.HTTPError, e
570                    req = urllib2.Request(url)
571                    if form_data:
572                        form_data = urllib.urlencode(form_data)
573                        req = urllib2.Request(url, form_data)
574                    req.add_header('User-Agent', self._user_agent)
575                    for k, v in headers.items():
576                        req.add_header(k, v)
577                    if compression:
578                        req.add_header('Accept-Encoding', 'gzip')
579                    response = urllib2.urlopen(req)
580                    return HttpResponse(response)
581                else:
582                    raise urllib2.HTTPError, e
583
584
585class HttpResponse:
586    '''
587    This class represents a response from an HTTP request.
588   
589    The content is examined and every attempt is made to properly encode it to
590    Unicode.
591   
592    .. seealso::
593        :meth:`Net.http_GET`, :meth:`Net.http_HEAD` and :meth:`Net.http_POST`
594    '''
595   
596    content = ''
597    '''Unicode encoded string containing the body of the response.'''
598   
599   
600    def __init__(self, response):
601        '''
602        Args:
603            response (:class:`mimetools.Message`): The object returned by a call
604            to :func:`urllib2.urlopen`.
605        '''
606        self._response = response
607        html = response.read()
608        try:
609            if response.headers['content-encoding'].lower() == 'gzip':
610                html = gzip.GzipFile(fileobj=StringIO.StringIO(html)).read()
611        except:
612            pass
613       
614        try:
615            content_type = response.headers['content-type']
616            if 'charset=' in content_type:
617                encoding = content_type.split('charset=')[-1]
618        except:
619            pass
620
621        r = re.search('<meta\s+http-equiv="Content-Type"\s+content="(?:.+?);' +
622                      '\s+charset=(.+?)"', html, re.IGNORECASE)
623        if r:
624            encoding = r.group(1) 
625                   
626        try:
627            html = unicode(html, encoding)
628        except:
629            pass
630       
631        #try:
632        #    if response.headers['content-encoding'].lower() == 'gzip':
633        #        r = re.search('<meta\s+http-equiv="Content-Type"\s+content="(?:.+?);' + '\s+charset=(.+?)"', html, re.IGNORECASE)
634        #        if r:
635        #               encoding = r.group(1)
636        #               try:
637        #                       html = unicode(html, encoding)
638        #               except:
639        #                       pass
640        #except:
641        #    pass
642           
643        self.content = html
644   
645   
646    def get_headers(self):
647        '''Returns a List of headers returned by the server.'''
648        return self._response.info().headers
649   
650       
651    def get_url(self):
652        '''
653        Return the URL of the resource retrieved, commonly used to determine if
654        a redirect was followed.
655        '''
656        return self._response.geturl()
Note: See TracBrowser for help on using the repository browser.