Completed
Push — master ( 3969ce...f11bd6 )
by John
02:45
created

check_prep()   B

Complexity

Conditions 1

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 32
ccs 5
cts 5
cp 1
crap 1
rs 8.8571
1
#!/usr/bin/env python3
2 5
"""This module is used for network connections; APIs, downloading, etc."""
3
4 5
import os  # filesystem read
5 5
try:
6 5
    from defusedxml import ElementTree  # safer XML parsing
7 1
except (ImportError, AttributeError):
8 1
    from xml.etree import ElementTree  # XML parsing
9 5
import re  # regexes
10 5
import concurrent.futures  # multiprocessing/threading
11 5
import glob  # pem file lookup
12 5
import hashlib  # salt
13 5
import time  # salt
14 5
import random  # salt
15 5
import base64  # encoding
16 5
import binascii  # encoding
17 5
import zlib  # encoding
18 5
import requests  # downloading
19 5
from bs4 import BeautifulSoup  # scraping
20 5
from bbarchivist import utilities  # parse filesize
21 5
from bbarchivist.bbconstants import SERVERS, TCLMASTERS  # lookup servers
22
23 5
__author__ = "Thurask"
24 5
__license__ = "WTFPL v2"
25 5
__copyright__ = "2015-2017 Thurask"
26
27
28 5
def grab_pem():
29
    """
30
    Work with either local cacerts or system cacerts.
31
    """
32 5
    try:
33 5
        pemfile = glob.glob(os.path.join(os.getcwd(), "cacert.pem"))[0]
34 5
    except IndexError:
35 5
        return requests.certs.where()  # no local cacerts
36
    else:
37 5
        return os.path.abspath(pemfile)  # local cacerts
38
39
40 5
def pem_wrapper(method):
41
    """
42
    Decorator to set REQUESTS_CA_BUNDLE.
43
44
    :param method: Method to use.
45
    :type method: function
46
    """
47 5
    def wrapper(*args, **kwargs):
48
        """
49
        Set REQUESTS_CA_BUNDLE before doing function.
50
        """
51 5
        os.environ["REQUESTS_CA_BUNDLE"] = grab_pem()
52 5
        return method(*args, **kwargs)
53 5
    return wrapper
54
55
56 5
def generic_session(session=None):
57
    """
58
    Create a Requests session object on the fly, if need be.
59
60
    :param session: Requests session object, created if this is None.
61
    :type session: requests.Session()
62
    """
63 5
    sess = requests.Session() if session is None else session
64 5
    return sess
65
66
67 5
def generic_soup_parser(url, session=None):
68
    """
69
    Get a BeautifulSoup HTML parser for some URL.
70
71
    :param url: The URL to check.
72
    :type url: str
73
74
    :param session: Requests session object, default is created on the fly.
75
    :type session: requests.Session()
76
    """
77 5
    session = generic_session(session)
78 5
    req = session.get(url)
79 5
    soup = BeautifulSoup(req.content, "html.parser")
80 5
    return soup
81
82
83 5
@pem_wrapper
84 5
def get_length(url, session=None):
85
    """
86
    Get content-length header from some URL.
87
88
    :param url: The URL to check.
89
    :type url: str
90
91
    :param session: Requests session object, default is created on the fly.
92
    :type session: requests.Session()
93
    """
94 5
    session = generic_session(session)
95 5
    if url is None:
96 5
        return 0
97 5
    try:
98 5
        heads = session.head(url)
99 5
        fsize = heads.headers['content-length']
100 5
        return int(fsize)
101 5
    except requests.ConnectionError:
102 5
        return 0
103
104
105 5
@pem_wrapper
106 5
def download(url, output_directory=None, session=None):
107
    """
108
    Download file from given URL.
109
110
    :param url: URL to download from.
111
    :type url: str
112
113
    :param output_directory: Download folder. Default is local.
114
    :type output_directory: str
115
116
    :param session: Requests session object, default is created on the fly.
117
    :type session: requests.Session()
118
    """
119 5
    session = generic_session(session)
120 5
    output_directory = utilities.dirhandler(output_directory, os.getcwd())
121 5
    lfname = url.split('/')[-1]
122 5
    sname = utilities.stripper(lfname)
123 5
    fname = os.path.join(output_directory, lfname)
124 5
    download_writer(url, fname, lfname, sname, session)
125 5
    remove_empty_download(fname)
126
127
128 5
def remove_empty_download(fname):
129
    """
130
    Remove file if it's empty.
131
132
    :param fname: File path.
133
    :type fname: str
134
    """
135 5
    if os.stat(fname).st_size == 0:
136 5
        os.remove(fname)
137
138
139 5
def download_writer(url, fname, lfname, sname, session=None):
140
    """
141
    Download file and write to disk.
142
143
    :param url: URL to download from.
144
    :type url: str
145
146
    :param fname: File path.
147
    :type fname: str
148
149
    :param lfname: Long filename.
150
    :type lfname: str
151
152
    :param sname: Short name, for printing to screen.
153
    :type sname: str
154
155
    :param session: Requests session object, default is created on the fly.
156
    :type session: requests.Session()
157
    """
158 5
    with open(fname, "wb") as file:
159 5
        req = session.get(url, stream=True)
160 5
        clength = req.headers['content-length']
161 5
        fsize = utilities.fsizer(clength)
162 5
        if req.status_code == 200:  # 200 OK
163 5
            print("DOWNLOADING {0} [{1}]".format(sname, fsize))
164 5
            for chunk in req.iter_content(chunk_size=1024):
165 5
                file.write(chunk)
166
        else:
167 5
            print("ERROR: HTTP {0} IN {1}".format(req.status_code, lfname))
168
169
170 5
def download_bootstrap(urls, outdir=None, workers=5, session=None):
171
    """
172
    Run downloaders for each file in given URL iterable.
173
174
    :param urls: URLs to download.
175
    :type urls: list
176
177
    :param outdir: Download folder. Default is handled in :func:`download`.
178
    :type outdir: str
179
180
    :param workers: Number of worker processes. Default is 5.
181
    :type workers: int
182
183
    :param session: Requests session object, default is created on the fly.
184
    :type session: requests.Session()
185
    """
186 5
    workers = len(urls) if len(urls) < workers else workers
187 5
    spinman = utilities.SpinManager()
188 5
    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as xec:
189 5
        try:
190 5
            spinman.start()
191 5
            for url in urls:
192 5
                xec.submit(download, url, outdir, session)
193 5
        except (KeyboardInterrupt, SystemExit):
194 5
            xec.shutdown()
195 5
            spinman.stop()
196 5
    spinman.stop()
197 5
    utilities.spinner_clear()
198 5
    utilities.line_begin()
199
200
201 5
def download_android_tools(downloaddir=None):
202
    """
203
    Download Android SDK platform tools.
204
205
    :param downloaddir: Directory name, default is "plattools".
206
    :type downloaddir: str
207
    """
208 5
    if downloaddir is None:
209 5
        downloaddir = "plattools"
210 5
    if os.path.exists(downloaddir):
211 5
        os.removedirs(downloaddir)
212 5
    os.mkdir(downloaddir)
213 5
    platforms = ("windows", "linux", "darwin")
214 5
    dlurls = ["https://dl.google.com/android/repository/platform-tools-latest-{0}.zip".format(plat) for plat in platforms]
215 5
    sess = generic_session()
216 5
    download_bootstrap(dlurls, outdir="plattools", session=sess)
217
218
219 5
@pem_wrapper
220 5
def getcode(url, session=None):
221
    """
222
    Return status code of given URL.
223
224
    :param url: URL to check.
225
    :type url: str
226
227
    :param session: Requests session object, default is created on the fly.
228
    :type session: requests.Session()
229
    """
230 5
    session = generic_session(session)
231 5
    try:
232 5
        shead = session.head(url)
233 5
        status = int(shead.status_code)
234 5
        return status
235 5
    except requests.ConnectionError:
236 5
        return 404
237
238
239 5
@pem_wrapper
240 5
def availability(url, session=None):
241
    """
242
    Check HTTP status code of given URL.
243
    200 or 301-308 is OK, else is not.
244
245
    :param url: URL to check.
246
    :type url: str
247
248
    :param session: Requests session object, default is created on the fly.
249
    :type session: requests.Session()
250
    """
251 5
    status = getcode(url, session)
252 5
    return status == 200 or 300 < status <= 308
253
254
255 5
def clean_availability(results, server):
256
    """
257
    Clean availability for autolookup script.
258
259
    :param results: Result dict.
260
    :type results: dict(str: str)
261
262
    :param server: Server, key for result dict.
263
    :type server: str
264
    """
265 5
    marker = "PD" if server == "p" else server.upper()
266 5
    rel = results[server.lower()]
267 5
    avail = marker if rel != "SR not in system" and rel is not None else "  "
268 5
    return rel, avail
269
270
271 5
def tcl_master():
272
    """
273
    Get a random master server.
274
    """
275 5
    return random.choice(TCLMASTERS)
276
277
278 5
def tcl_default_id(devid):
279
    """
280
    Get an IMEI or a serial number or something.
281
282
    :param devid: Return default if this is None.
283
    :type devid: str
284
    """
285 5
    if devid is None:
286 5
        devid = "543212345000000"
287 5
    return devid
288
289
290 5
def check_prep(curef, mode=4, fvver="AAA000", cltp=2010, cktp=2, rtd=1, chnl=2, devid=None):
291
    """
292
    Prepare variables for TCL update check.
293
294
    :param curef: PRD of the phone variant to check.
295
    :type curef: str
296
297
    :param mode: 4 if downloading autoloaders, 2 if downloading OTA deltas.
298
    :type mode: int
299
300
    :param fvver: Initial software version, must be specific if downloading OTA deltas.
301
    :type fvver: str
302
303
    :param cltp: 2010 to always show latest version, 10 to show actual updates. Default is 2010.
304
    :type cltp: int
305
306
    :param cktp: 2 if checking manually, 1 if checking automatically. Default is 2.
307
    :type cktp: int
308
309
    :param rtd: 2 if rooted, 1 if not. Default is 1.
310
    :type rtd: int
311
312
    :param chnl: 2 if checking on WiFi, 1 if checking on mobile. Default is 2.
313
    :type chnl: int
314
315
    :param devid: Serial number/IMEI. Default is fake, not that it matters.
316
    :type devid: str
317
    """
318 5
    devid = tcl_default_id(devid)
319 5
    geturl = "http://{0}/check.php".format(tcl_master())
320 5
    params = {"id": devid, "curef": curef, "fv": fvver, "mode": mode, "type": "Firmware", "cltp": cltp, "cktp": cktp, "rtd": rtd, "chnl": chnl}
321 5
    return geturl, params
322
323
324 5
@pem_wrapper
325 5
def tcl_check(curef, session=None, mode=4, fvver="AAA000"):
326
    """
327
    Check TCL server for updates.
328
329
    :param curef: PRD of the phone variant to check.
330
    :type curef: str
331
332
    :param session: Requests session object, default is created on the fly.
333
    :type session: requests.Session()
334
335
    :param mode: 4 if downloading autoloaders, 2 if downloading OTA deltas.
336
    :type mode: int
337
338
    :param fvver: Initial software version, must be specific if downloading OTA deltas.
339
    :type fvver: str
340
    """
341 5
    sess = generic_session(session)
342 5
    geturl, params = check_prep(curef, mode, fvver)
343 5
    req = sess.get(geturl, params=params)
344 5
    if req.status_code == 200:
345 5
        return req.text
346
    else:
347 5
        return None
348
349
350 5
def parse_tcl_check(data):
351
    """
352
    Extract version and file info from TCL update server response.
353
354
    :param data: The data to parse.
355
    :type data: str
356
    """
357 5
    root = ElementTree.fromstring(data)
358 5
    tvver = root.find("VERSION").find("TV").text
359 5
    fwid = root.find("FIRMWARE").find("FW_ID").text
360 5
    fileinfo = root.find("FIRMWARE").find("FILESET").find("FILE")
361 5
    filename = fileinfo.find("FILENAME").text
362 5
    filesize = fileinfo.find("SIZE").text
363 5
    filehash = fileinfo.find("CHECKSUM").text
364 5
    return tvver, fwid, filename, filesize, filehash
365
366
367 5
def tcl_salt():
368
    """
369
    Generate salt value for TCL server tools.
370
    """
371 5
    millis = round(time.time() * 1000)
372 5
    tail = "{0:06d}".format(random.randint(0, 999999))
373 5
    return "{0}{1}".format(str(millis), tail)
374
375
376 5
def unpack_vdkey():
377
    """
378
    Draw the curtain back.
379
    """
380 5
    vdkey = b"eJwdjwEOwDAIAr8kKFr//7HhmqXp8AIIDrYAgg8byiUXrwRJRXja+d6iNxu0AhUooDCN9rd6rDLxmGIakUVWo3IGCTRWqCAt6X4jGEIUAxgN0eYWnp+LkpHQAg/PsO90ELsy0Npm/n2HbtPndFgGEV31R9OmT4O4nrddjc3Qt6nWscx7e+WRHq5UnOudtjw5skuV09pFhvmqnOEIs4ljPeel1wfLYUF4\n"
381 5
    vdk = zlib.decompress(binascii.a2b_base64(vdkey))
382 5
    return vdk.decode("utf-8")
383
384
385 5
def vkhash(curef, tvver, fwid, salt, mode=4, fvver="AAA000", cltp=2010, devid=None):
386
    """
387
    Generate hash from TCL update server variables.
388
389
    :param curef: PRD of the phone variant to check.
390
    :type curef: str
391
392
    :param tvver: Target software version.
393
    :type tvver: str
394
395
    :param fwid: Firmware ID for desired download file.
396
    :type fwid: str
397
398
    :param salt: Salt hash.
399
    :type salt: str
400
401
    :param mode: 4 if downloading autoloaders, 2 if downloading OTA deltas.
402
    :type mode: int
403
404
    :param fvver: Initial software version, must be specific if downloading OTA deltas.
405
    :type fvver: str
406
407
    :param cltp: 2010 to always show latest version, 10 to show actual updates. Default is 2010.
408
    :type cltp: int
409
410
    :param devid: Serial number/IMEI. Default is fake, not that it matters.
411
    :type devid: str
412
    """
413 5
    vdk = unpack_vdkey()
414 5
    devid = tcl_default_id(devid)
415 5
    query = "id={0}&salt={1}&curef={2}&fv={3}&tv={4}&type={5}&fw_id={6}&mode={7}&cltp={8}{9}".format(devid, salt, curef, fvver, tvver, "Firmware", fwid, mode, cltp, vdk)
416 5
    engine = hashlib.sha1()
417 5
    engine.update(bytes(query, "utf-8"))
418 5
    return engine.hexdigest()
419
420
421 5
def download_request_prep(curef, tvver, fwid, salt, vkh, mode=4, fvver="AAA000", cltp=2010, devid=None):
422
    """
423
    Prepare variables for download server check.
424
425
    :param curef: PRD of the phone variant to check.
426
    :type curef: str
427
428
    :param tvver: Target software version.
429
    :type tvver: str
430
431
    :param fwid: Firmware ID for desired download file.
432
    :type fwid: str
433
434
    :param salt: Salt hash.
435
    :type salt: str
436
437
    :param vkh: VDKey-based hash.
438
    :type vkh: str
439
440
    :param mode: 4 if downloading autoloaders, 2 if downloading OTA deltas.
441
    :type mode: int
442
443
    :param fvver: Initial software version, must be specific if downloading OTA deltas.
444
    :type fvver: str
445
446
    :param cltp: 2010 to always show latest version, 10 to show actual updates. Default is 2010.
447
    :type cltp: int
448
449
    :param devid: Serial number/IMEI. Default is fake, not that it matters.
450
    :type devid: str
451
    """
452 5
    devid = tcl_default_id(devid)
453 5
    posturl = "http://{0}/download_request.php".format(tcl_master())
454 5
    params = {"id": devid, "curef": curef, "fv": fvver, "mode": mode, "type": "Firmware", "tv": tvver, "fw_id": fwid, "salt": salt, "vk": vkh, "cltp": cltp}
455 5
    if mode == 4:
456 5
        params["foot"] = 1
457 5
    return posturl, params
458
459
460 5
@pem_wrapper
461 5
def tcl_download_request(curef, tvver, fwid, salt, vkh, session=None, mode=4, fvver="AAA000"):
462
    """
463
    Check TCL server for download URLs.
464
465
    :param curef: PRD of the phone variant to check.
466
    :type curef: str
467
468
    :param tvver: Target software version.
469
    :type tvver: str
470
471
    :param fwid: Firmware ID for desired download file.
472
    :type fwid: str
473
474
    :param salt: Salt hash.
475
    :type salt: str
476
477
    :param vkh: VDKey-based hash.
478
    :type vkh: str
479
480
    :param session: Requests session object, default is created on the fly.
481
    :type session: requests.Session()
482
483
    :param mode: 4 if downloading autoloaders, 2 if downloading OTA deltas.
484
    :type mode: int
485
486
    :param fvver: Initial software version, must be specific if downloading OTA deltas.
487
    :type fvver: str
488
    """
489 5
    sess = generic_session(session)
490 5
    posturl, params = download_request_prep(curef, tvver, fwid, salt, vkh, mode, fvver)
491 5
    req = sess.post(posturl, data=params)
492 5
    if req.status_code == 200:
493 5
        return req.text
494
    else:
495 5
        return None
496
497
498 5
def parse_tcl_download_request(body, mode=4):
499
    """
500
    Extract file URL and encrypt slave URL from TCL update server response.
501
502
    :param data: The data to parse.
503
    :type data: str
504
505
    :param mode: 4 if downloading autoloaders, 2 if downloading OTA deltas.
506
    :type mode: int
507
    """
508 5
    root = ElementTree.fromstring(body)
509 5
    slavelist = root.find("SLAVE_LIST").findall("SLAVE")
510 5
    slave = random.choice(slavelist).text
511 5
    dlurl = root.find("FILE_LIST").find("FILE").find("DOWNLOAD_URL").text
512 5
    eslave = root.find("SLAVE_LIST").findall("ENCRYPT_SLAVE")
513 5
    encslave = None if mode == 2 or not eslave else random.choice(eslave).text
514 5
    return "http://{0}{1}".format(slave, dlurl), encslave
515
516
517 5
def encrypt_header_prep(address, encslave):
518
    """
519
    Prepare variables for encrypted header check.
520
521
    :param address: File URL minus host.
522
    :type address: str
523
524
    :param encslave: Server hosting header script.
525
    :type encslave: str
526
    """
527 5
    encs = {b"YWNjb3VudA==" : b"emhlbmdodWEuZ2Fv", b"cGFzc3dvcmQ=": b"cWFydUQ0b2s="}
528 5
    params = {base64.b64decode(key): base64.b64decode(val) for key, val in encs.items()}
529 5
    params[b"address"] = bytes(address, "utf-8")
530 5
    posturl = "http://{0}/encrypt_header.php".format(encslave)
531 5
    return posturl, params
532
533
534 5
@pem_wrapper
535 5
def encrypt_header(address, encslave, session=None):
536
    """
537
    Check encrypted header.
538
539
    :param address: File URL minus host.
540
    :type address: str
541
542
    :param encslave: Server hosting header script.
543
    :type encslave: str
544
545
    :param session: Requests session object, default is created on the fly.
546
    :type session: requests.Session()
547
    """
548 5
    sess = generic_session(session)
549 5
    posturl, params = encrypt_header_prep(address, encslave)
550 5
    req = sess.post(posturl, data=params)
551 5
    if req.status_code == 206:  # partial
552 5
        contentlength = int(req.headers["Content-Length"])
553 5
        sentinel = "HEADER FOUND" if contentlength == 4194320 else "NO HEADER FOUND"
554 5
        return sentinel
555
    else:
556 5
        return None
557
558
559 5
def cchecker_get_tags(root):
560
    """
561
    Get country and carrier from XML.
562
563
    :param root: ElementTree we're barking up.
564
    :type root: xml.etree.ElementTree.ElementTree
565
    """
566 5
    for child in root:
567 5
        if child.tag == "country":
568 5
            country = child.get("name")
569 5
        if child.tag == "carrier":
570 5
            carrier = child.get("name")
571 5
    return country, carrier
572
573
574 5
@pem_wrapper
575 5
def carrier_checker(mcc, mnc, session=None):
576
    """
577
    Query BlackBerry World to map a MCC and a MNC to a country and carrier.
578
579
    :param mcc: Country code.
580
    :type mcc: int
581
582
    :param mnc: Network code.
583
    :type mnc: int
584
585
    :param session: Requests session object, default is created on the fly.
586
    :type session: requests.Session()
587
    """
588 5
    session = generic_session(session)
589 5
    url = "http://appworld.blackberry.com/ClientAPI/checkcarrier?homemcc={0}&homemnc={1}&devicevendorid=-1&pin=0".format(mcc, mnc)
590 5
    user_agent = {'User-agent': 'AppWorld/5.1.0.60'}
591 5
    req = session.get(url, headers=user_agent)
592 5
    root = ElementTree.fromstring(req.text)
593 5
    country, carrier = cchecker_get_tags(root)
594 5
    return country, carrier
595
596
597 5
def return_npc(mcc, mnc):
598
    """
599
    Format MCC and MNC into a NPC.
600
601
    :param mcc: Country code.
602
    :type mcc: int
603
604
    :param mnc: Network code.
605
    :type mnc: int
606
    """
607 5
    return "{0}{1}30".format(str(mcc).zfill(3), str(mnc).zfill(3))
608
609
610 5
@pem_wrapper
611 5
def carrier_query(npc, device, upgrade=False, blitz=False, forced=None, session=None):
612
    """
613
    Query BlackBerry servers, check which update is out for a carrier.
614
615
    :param npc: MCC + MNC (see `func:return_npc`)
616
    :type npc: int
617
618
    :param device: Hexadecimal hardware ID.
619
    :type device: str
620
621
    :param upgrade: Whether to use upgrade files. False by default.
622
    :type upgrade: bool
623
624
    :param blitz: Whether or not to create a blitz package. False by default.
625
    :type blitz: bool
626
627
    :param forced: Force a software release.
628
    :type forced: str
629
630
    :param session: Requests session object, default is created on the fly.
631
    :type session: requests.Session()
632
    """
633 5
    session = generic_session(session)
634 5
    upg = "upgrade" if upgrade else "repair"
635 5
    forced = "latest" if forced is None else forced
636 5
    url = "https://cs.sl.blackberry.com/cse/updateDetails/2.2/"
637 5
    query = '<?xml version="1.0" encoding="UTF-8"?>'
638 5
    query += '<updateDetailRequest version="2.2.1" authEchoTS="1366644680359">'
639 5
    query += "<clientProperties>"
640 5
    query += "<hardware>"
641 5
    query += "<pin>0x2FFFFFB3</pin><bsn>1128121361</bsn>"
642 5
    query += "<imei>004401139269240</imei>"
643 5
    query += "<id>0x{0}</id>".format(device)
644 5
    query += "</hardware>"
645 5
    query += "<network>"
646 5
    query += "<homeNPC>0x{0}</homeNPC>".format(npc)
647 5
    query += "<iccid>89014104255505565333</iccid>"
648 5
    query += "</network>"
649 5
    query += "<software>"
650 5
    query += "<currentLocale>en_US</currentLocale>"
651 5
    query += "<legalLocale>en_US</legalLocale>"
652 5
    query += "</software>"
653 5
    query += "</clientProperties>"
654 5
    query += "<updateDirectives>"
655 5
    query += '<allowPatching type="REDBEND">true</allowPatching>'
656 5
    query += "<upgradeMode>{0}</upgradeMode>".format(upg)
657 5
    query += "<provideDescriptions>false</provideDescriptions>"
658 5
    query += "<provideFiles>true</provideFiles>"
659 5
    query += "<queryType>NOTIFICATION_CHECK</queryType>"
660 5
    query += "</updateDirectives>"
661 5
    query += "<pollType>manual</pollType>"
662 5
    query += "<resultPackageSetCriteria>"
663 5
    query += '<softwareRelease softwareReleaseVersion="{0}" />'.format(forced)
664 5
    query += "<releaseIndependent>"
665 5
    query += '<packageType operation="include">application</packageType>'
666 5
    query += "</releaseIndependent>"
667 5
    query += "</resultPackageSetCriteria>"
668 5
    query += "</updateDetailRequest>"
669 5
    header = {"Content-Type": "text/xml;charset=UTF-8"}
670 5
    req = session.post(url, headers=header, data=query)
671 5
    return parse_carrier_xml(req.text, blitz)
672
673
674 5
def carrier_swver_get(root):
675
    """
676
    Get software release from carrier XML.
677
678
    :param root: ElementTree we're barking up.
679
    :type root: xml.etree.ElementTree.ElementTree
680
    """
681 5
    for child in root.iter("softwareReleaseMetadata"):
682 5
        swver = child.get("softwareReleaseVersion")
683 5
    return swver
684
685
686 5
def carrier_child_fileappend(child, files, baseurl, blitz=False):
687
    """
688
    Append bar file links to a list from a child element.
689
690
    :param child: Child element in use.
691
    :type child: xml.etree.ElementTree.Element
692
693
    :param files: Filelist.
694
    :type files: list(str)
695
696
    :param baseurl: Base URL, URL minus the filename.
697
    :type baseurl: str
698
699
    :param blitz: Whether or not to create a blitz package. False by default.
700
    :type blitz: bool
701
    """
702 5
    if not blitz:
703 5
        files.append(baseurl + child.get("path"))
704
    else:
705 5
        if child.get("type") not in ["system:radio", "system:desktop", "system:os"]:
706 5
            files.append(baseurl + child.get("path"))
707 5
    return files
708
709
710 5
def carrier_child_finder(root, files, baseurl, blitz=False):
711
    """
712
    Extract filenames, radio and OS from child elements.
713
714
    :param root: ElementTree we're barking up.
715
    :type root: xml.etree.ElementTree.ElementTree
716
717
    :param files: Filelist.
718
    :type files: list(str)
719
720
    :param baseurl: Base URL, URL minus the filename.
721
    :type baseurl: str
722
723
    :param blitz: Whether or not to create a blitz package. False by default.
724
    :type blitz: bool
725
    """
726 5
    osver = radver = ""
727 5
    for child in root.iter("package"):
728 5
        files = carrier_child_fileappend(child, files, baseurl, blitz)
729 5
        if child.get("type") == "system:radio":
730 5
            radver = child.get("version")
731 5
        elif child.get("type") == "system:desktop":
732 5
            osver = child.get("version")
733 5
        elif child.get("type") == "system:os":
734 5
            osver = child.get("version")
735 5
    return osver, radver, files
736
737
738 5
def parse_carrier_xml(data, blitz=False):
739
    """
740
    Parse the response to a carrier update request and return the juicy bits.
741
742
    :param data: The data to parse.
743
    :type data: xml
744
745
    :param blitz: Whether or not to create a blitz package. False by default.
746
    :type blitz: bool
747
    """
748 5
    root = ElementTree.fromstring(data)
749 5
    sw_exists = root.find('./data/content/softwareReleaseMetadata')
750 5
    swver = "N/A" if sw_exists is None else ""
751 5
    if sw_exists is not None:
752 5
        swver = carrier_swver_get(root)
753 5
    files = []
754 5
    package_exists = root.find('./data/content/fileSets/fileSet')
755 5
    osver = radver = ""
756 5
    if package_exists is not None:
757 5
        baseurl = "{0}/".format(package_exists.get("url"))
758 5
        osver, radver, files = carrier_child_finder(root, files, baseurl, blitz)
759 5
    return(swver, osver, radver, files)
760
761
762 5
@pem_wrapper
763 5
def sr_lookup(osver, server, session=None):
764
    """
765
    Software release lookup, with choice of server.
766
    :data:`bbarchivist.bbconstants.SERVERLIST` for server list.
767
768
    :param osver: OS version to lookup, 10.x.y.zzzz.
769
    :type osver: str
770
771
    :param server: Server to use.
772
    :type server: str
773
774
    :param session: Requests session object, default is created on the fly.
775
    :type session: requests.Session()
776
    """
777 5
    query = '<?xml version="1.0" encoding="UTF-8"?>'
778 5
    query += '<srVersionLookupRequest version="2.0.0"'
779 5
    query += ' authEchoTS="1366644680359">'
780 5
    query += '<clientProperties><hardware>'
781 5
    query += '<pin>0x2FFFFFB3</pin><bsn>1140011878</bsn>'
782 5
    query += '<imei>004402242176786</imei><id>0x8D00240A</id>'
783 5
    query += '<isBootROMSecure>true</isBootROMSecure>'
784 5
    query += '</hardware>'
785 5
    query += '<network>'
786 5
    query += '<vendorId>0x0</vendorId><homeNPC>0x60</homeNPC>'
787 5
    query += '<currentNPC>0x60</currentNPC><ecid>0x1</ecid>'
788 5
    query += '</network>'
789 5
    query += '<software><currentLocale>en_US</currentLocale>'
790 5
    query += '<legalLocale>en_US</legalLocale>'
791 5
    query += '<osVersion>{0}</osVersion>'.format(osver)
792 5
    query += '<omadmEnabled>false</omadmEnabled>'
793 5
    query += '</software></clientProperties>'
794 5
    query += '</srVersionLookupRequest>'
795 5
    reqtext = sr_lookup_poster(query, server, session)
796 5
    packtext = sr_lookup_xmlparser(reqtext)
797 5
    return packtext
798
799
800 5
def sr_lookup_poster(query, server, session=None):
801
    """
802
    Post the XML payload for a software release lookup.
803
804
    :param query: XML payload.
805
    :type query: str
806
807
    :param server: Server to use.
808
    :type server: str
809
810
    :param session: Requests session object, default is created on the fly.
811
    :type session: requests.Session()
812
    """
813 5
    session = generic_session(session)
814 5
    header = {"Content-Type": "text/xml;charset=UTF-8"}
815 5
    try:
816 5
        req = session.post(server, headers=header, data=query, timeout=1)
817 5
    except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
818 5
        reqtext = "SR not in system"
819
    else:
820 5
        reqtext = req.text
821 5
    return reqtext
822
823
824 5
def sr_lookup_xmlparser(reqtext):
825
    """
826
    Take the text of a software lookup request response and parse it as XML.
827
828
    :param reqtext: Response text, hopefully XML formatted.
829
    :type reqtext: str
830
    """
831 5
    try:
832 5
        root = ElementTree.fromstring(reqtext)
833 5
    except ElementTree.ParseError:
834 5
        packtext = "SR not in system"
835
    else:
836 5
        packtext = sr_lookup_extractor(root)
837 5
    return packtext
838
839
840 5
def sr_lookup_extractor(root):
841
    """
842
    Take an ElementTree and extract a software release from it.
843
844
    :param root: ElementTree we're barking up.
845
    :type root: xml.etree.ElementTree.ElementTree
846
    """
847 5
    reg = re.compile(r"(\d{1,4}\.)(\d{1,4}\.)(\d{1,4}\.)(\d{1,4})")
848 5
    packages = root.findall('./data/content/')
849 5
    for package in packages:
850 5
        if package.text is not None:
851 5
            match = reg.match(package.text)
852 5
            packtext = package.text if match else "SR not in system"
853 5
            return packtext
854
855
856 5
def sr_lookup_bootstrap(osv, session=None, no2=False):
857
    """
858
    Run lookups for each server for given OS.
859
860
    :param osv: OS to check.
861
    :type osv: str
862
863
    :param session: Requests session object, default is created on the fly.
864
    :type session: requests.Session()
865
866
    :param no2: Whether to skip Alpha2/Beta2 servers. Default is false.
867
    :type no2: bool
868
    """
869 5
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as xec:
870 5
        try:
871 5
            results = {
872
                "p": None,
873
                "a1": None,
874
                "a2": None,
875
                "b1": None,
876
                "b2": None
877
            }
878 5
            if no2:
879 5
                del results["a2"]
880 5
                del results["b2"]
881 5
            for key in results:
882 5
                results[key] = xec.submit(sr_lookup, osv, SERVERS[key], session).result()
883 5
            return results
884 5
        except KeyboardInterrupt:
885 5
            xec.shutdown(wait=False)
886
887
888 5
@pem_wrapper
889 5
def available_bundle_lookup(mcc, mnc, device, session=None):
890
    """
891
    Check which software releases were ever released for a carrier.
892
893
    :param mcc: Country code.
894
    :type mcc: int
895
896
    :param mnc: Network code.
897
    :type mnc: int
898
899
    :param device: Hexadecimal hardware ID.
900
    :type device: str
901
902
    :param session: Requests session object, default is created on the fly.
903
    :type session: requests.Session()
904
    """
905 5
    session = generic_session(session)
906 5
    server = "https://cs.sl.blackberry.com/cse/availableBundles/1.0.0/"
907 5
    npc = return_npc(mcc, mnc)
908 5
    query = '<?xml version="1.0" encoding="UTF-8"?>'
909 5
    query += '<availableBundlesRequest version="1.0.0" '
910 5
    query += 'authEchoTS="1366644680359">'
911 5
    query += '<deviceId><pin>0x2FFFFFB3</pin></deviceId>'
912 5
    query += '<clientProperties><hardware><id>0x{0}</id>'.format(device)
913 5
    query += '<isBootROMSecure>true</isBootROMSecure></hardware>'
914 5
    query += '<network><vendorId>0x0</vendorId><homeNPC>0x{0}</homeNPC>'.format(npc)
915 5
    query += '<currentNPC>0x{0}</currentNPC></network><software>'.format(npc)
916 5
    query += '<currentLocale>en_US</currentLocale>'
917 5
    query += '<legalLocale>en_US</legalLocale>'
918 5
    query += '<osVersion>10.0.0.0</osVersion>'
919 5
    query += '<radioVersion>10.0.0.0</radioVersion></software>'
920 5
    query += '</clientProperties><updateDirectives><bundleVersionFilter>'
921 5
    query += '</bundleVersionFilter></updateDirectives>'
922 5
    query += '</availableBundlesRequest>'
923 5
    header = {"Content-Type": "text/xml;charset=UTF-8"}
924 5
    req = session.post(server, headers=header, data=query)
925 5
    root = ElementTree.fromstring(req.text)
926 5
    package = root.find('./data/content')
927 5
    bundlelist = [child.attrib["version"] for child in package]
928 5
    return bundlelist
929
930
931 5
@pem_wrapper
932 5
def ptcrb_scraper(ptcrbid, session=None):
933
    """
934
    Get the PTCRB results for a given device.
935
936
    :param ptcrbid: Numerical ID from PTCRB (end of URL).
937
    :type ptcrbid: str
938
939
    :param session: Requests session object, default is created on the fly.
940
    :type session: requests.Session()
941
    """
942 5
    baseurl = "https://www.ptcrb.com/certified-devices/device-details/?model={0}".format(ptcrbid)
943 5
    sess = generic_session(session)
944 5
    sess.headers.update({"User-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36"})
945 5
    soup = generic_soup_parser(baseurl, sess)
946 5
    certtable = soup.find_all("table")[1]
947 5
    tds = certtable.find_all("td")[1::2]  # every other
948 5
    prelimlist = [tdx.text for tdx in tds]
949 5
    cleanlist = [ptcrb_item_cleaner(item.strip()) for item in prelimlist]
950 5
    return cleanlist
951
952
953 5
def space_pad(instring, minlength):
954
    """
955
    Pad a string with spaces until it's the minimum length.
956
957
    :param instring: String to pad.
958
    :type instring: str
959
960
    :param minlength: Pad while len(instring) < minlength.
961
    :type minlength: int
962
    """
963 5
    while len(instring) < minlength:
964 5
        instring += " "
965 5
    return instring
966
967
968 5
def ptcrb_cleaner_multios(item):
969
    """
970
    Discard multiple entries for "OS".
971
972
    :param item: The item to clean.
973
    :type item: str
974
    """
975 5
    if item.count("OS") > 1:
976 5
        templist = item.split("OS")
977 5
        templist[0] = "OS"
978 5
        item = "".join([templist[0], templist[1]])
979 5
    return item
980
981
982 5
def ptcrb_cleaner_spaces(item):
983
    """
984
    Pad item with spaces to the right length.
985
986
    :param item: The item to clean.
987
    :type item: str
988
    """
989 5
    spaclist = item.split(" ")
990 5
    if len(spaclist) > 1:
991 5
        spaclist[1] = space_pad(spaclist[1], 11)
992 5
    if len(spaclist) > 3:
993 5
        spaclist[3] = space_pad(spaclist[3], 11)
994 5
    item = " ".join(spaclist)
995 5
    return item
996
997
998 5
def ptcrb_item_cleaner(item):
999
    """
1000
    Cleanup poorly formatted PTCRB entries written by an intern.
1001
1002
    :param item: The item to clean.
1003
    :type item: str
1004
    """
1005 5
    item = item.replace("<td>", "")
1006 5
    item = item.replace("</td>", "")
1007 5
    item = item.replace("\n", "")
1008 5
    item = item.replace("SW: OS", "OS")
1009 5
    item = item.replace("Software Version: OS", "OS")
1010 5
    item = item.replace(" (SR", ", SR")
1011 5
    item = re.sub(r"\s?\((.*)$", "", item)
1012 5
    item = re.sub(r"\sSV.*$", "", item)
1013 5
    item = item.replace(")", "")
1014 5
    item = item.replace(". ", ".")
1015 5
    item = item.replace(";", "")
1016 5
    item = item.replace("version", "Version")
1017 5
    item = item.replace("Verison", "Version")
1018 5
    item = ptcrb_cleaner_multios(item)
1019 5
    item = item.replace("SR10", "SR 10")
1020 5
    item = item.replace("SR", "SW Release")
1021 5
    item = item.replace(" Version:", ":")
1022 5
    item = item.replace("Version ", " ")
1023 5
    item = item.replace(":1", ": 1")
1024 5
    item = item.replace(", ", " ")
1025 5
    item = item.replace(",", " ")
1026 5
    item = item.replace("Software", "SW")
1027 5
    item = item.replace("  ", " ")
1028 5
    item = item.replace("OS ", "OS: ")
1029 5
    item = item.replace("Radio ", "Radio: ")
1030 5
    item = item.replace("Release ", "Release: ")
1031 5
    item = ptcrb_cleaner_spaces(item)
1032 5
    item = item.strip()
1033 5
    item = item.replace("\r", "")
1034 5
    if item.startswith("10"):
1035 5
        item = "OS: {0}".format(item)
1036 5
    item = item.replace(":    ", ": ")
1037 5
    item = item.replace(":   ", ": ")
1038 5
    return item
1039
1040
1041 5
@pem_wrapper
1042 5
def kernel_scraper(utils=False, session=None):
1043
    """
1044
    Scrape BlackBerry's GitHub kernel repo for available branches.
1045
1046
    :param utils: Check android-utils repo instead of android-linux-kernel. Default is False.
1047
    :type utils: bool
1048
1049
    :param session: Requests session object, default is created on the fly.
1050
    :type session: requests.Session()
1051
    """
1052 5
    repo = "android-utils" if utils else "android-linux-kernel"
1053 5
    kernlist = []
1054 5
    sess = generic_session(session)
1055 5
    for page in range(1, 10):
1056 5
        url = "https://github.com/blackberry/{0}/branches/all?page={1}".format(repo, page)
1057 5
        soup = generic_soup_parser(url, sess)
1058 5
        if soup.find("div", {"class": "no-results-message"}):
1059 5
            break
1060
        else:
1061 5
            text = soup.get_text()
1062 5
            kernlist.extend(re.findall(r"msm[0-9]{4}\/[A-Z0-9]{6}", text, re.IGNORECASE))
1063 5
    return kernlist
1064
1065
1066 5
def root_generator(folder, build, variant="common"):
1067
    """
1068
    Generate roots for the SHAxxx hash lookup URLs.
1069
1070
    :param folder: Dictionary of variant: loader name pairs.
1071
    :type folder: dict(str: str)
1072
1073
    :param build: Build to check, 3 letters + 3 numbers.
1074
    :type build: str
1075
1076
    :param variant: Autoloader variant. Default is "common".
1077
    :type variant: str
1078
    """
1079
    #Priv specific
1080 5
    privx = "bbfoundation/hashfiles_priv/{0}".format(folder[variant])
1081
    #DTEK50 specific
1082 5
    dtek50x = "bbSupport/DTEK50" if build[:3] == "AAF" else "bbfoundation/hashfiles_priv/dtek50"
1083
    #DTEK60 specific
1084 5
    dtek60x = dtek50x  # still uses dtek50 folder, for some reason
1085
    #Pack it up
1086 5
    roots = {"Priv": privx, "DTEK50": dtek50x, "DTEK60": dtek60x}
1087 5
    return roots
1088
1089
1090 5
def make_droid_skeleton_bbm(method, build, device, variant="common"):
1091
    """
1092
    Make an Android autoloader/hash URL, on the BB Mobile site.
1093
1094
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
1095
    :type method: str
1096
1097
    :param build: Build to check, 3 letters + 3 numbers.
1098
    :type build: str
1099
1100
    :param device: Device to check.
1101
    :type device: str
1102
1103
    :param variant: Autoloader variant. Default is "common".
1104
    :type variant: str
1105
    """
1106 5
    devices = {"KEYone": "qc8953"}
1107 5
    base = "bbry_{2}_autoloader_user-{0}-{1}".format(variant, build.upper(), devices[device])
1108 5
    if method is None:
1109 5
        skel = "http://54.247.87.13/softwareupgrade/BBM/{0}.zip".format(base)
1110
    else:
1111 5
        skel = "http://54.247.87.13/softwareupgrade/BBM/{0}.{1}sum".format(base, method.lower())
1112 5
    return skel
1113
1114
1115 5
def make_droid_skeleton_og(method, build, device, variant="common"):
1116
    """
1117
    Make an Android autoloader/hash URL, on the original site.
1118
1119
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
1120
    :type method: str
1121
1122
    :param build: Build to check, 3 letters + 3 numbers.
1123
    :type build: str
1124
1125
    :param device: Device to check.
1126
    :type device: str
1127
1128
    :param variant: Autoloader variant. Default is "common".
1129
    :type variant: str
1130
    """
1131 5
    folder = {"vzw-vzw": "verizon", "na-att": "att", "na-tmo": "tmo", "common": "default"}
1132 5
    devices = {"Priv": "qc8992", "DTEK50": "qc8952_64_sfi", "DTEK60": "qc8996"}
1133 5
    roots = root_generator(folder, build, variant)
1134 5
    base = "bbry_{2}_autoloader_user-{0}-{1}".format(variant, build.upper(), devices[device])
1135 5
    if method is None:
1136 5
        skel = "https://bbapps.download.blackberry.com/Priv/{0}.zip".format(base)
1137
    else:
1138 5
        skel = "https://ca.blackberry.com/content/dam/{1}/{0}.{2}sum".format(base, roots[device], method.lower())
1139 5
    return skel
1140
1141
1142 5
def make_droid_skeleton(method, build, device, variant="common"):
1143
    """
1144
    Make an Android autoloader/hash URL.
1145
1146
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
1147
    :type method: str
1148
1149
    :param build: Build to check, 3 letters + 3 numbers.
1150
    :type build: str
1151
1152
    :param device: Device to check.
1153
    :type device: str
1154
1155
    :param variant: Autoloader variant. Default is "common".
1156
    :type variant: str
1157
    """
1158
    # No Aurora
1159 5
    oglist = ("Priv", "DTEK50", "DTEK60")  # BlackBerry
1160 5
    bbmlist = ("KEYone")   # BB Mobile
1161 5
    if device in oglist:
1162 5
        skel = make_droid_skeleton_og(method, build, device, variant)
1163 5
    elif device in bbmlist:
1164 5
        skel = make_droid_skeleton_bbm(method, build, device, variant)
1165 5
    return skel
1166
1167
1168 5
def bulk_droid_skeletons(devs, build, method=None):
1169
    """
1170
    Prepare list of Android autoloader/hash URLs.
1171
1172
    :param devs: List of devices.
1173
    :type devs: list(str)
1174
1175
    :param build: Build to check, 3 letters + 3 numbers.
1176
    :type build: str
1177
1178
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
1179
    :type method: str
1180
    """
1181 5
    carrier_variants = {
1182
        "Priv": ("common", "vzw-vzw", "na-tmo", "na-att"),
1183
        "KEYone": ("common", "usa-vzw", "usa-sprint", "global-att")  # verify this
1184
    }
1185 5
    common_variants = ("common", )  # for single-variant devices
1186 5
    carrier_devices = ("Priv", )  # add KEYone when verified
1187 5
    skels = []
1188 5
    for dev in devs:
1189 5
        varlist = carrier_variants[dev] if dev in carrier_devices else common_variants
1190 5
        for var in varlist:
1191 5
            skel = make_droid_skeleton(method, build, dev, var)
1192 5
            skels.append(skel)
1193 5
    return skels
1194
1195
1196 5
def prepare_droid_list(device):
1197
    """
1198
    Convert single devices to a list, if necessary.
1199
1200
    :param device: Device to check.
1201
    :type device: str
1202
    """
1203 5
    if isinstance(device, list):
1204 5
        devs = device
1205
    else:
1206 5
        devs = [device]
1207 5
    return devs
1208
1209
1210 5
def droid_scanner(build, device, method=None, session=None):
1211
    """
1212
    Check for Android autoloaders on BlackBerry's site.
1213
1214
    :param build: Build to check, 3 letters + 3 numbers.
1215
    :type build: str
1216
1217
    :param device: Device to check.
1218
    :type device: str
1219
1220
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
1221
    :type method: str
1222
1223
    :param session: Requests session object, default is created on the fly.
1224
    :type session: requests.Session()
1225
    """
1226 5
    devs = prepare_droid_list(device)
1227 5
    skels = bulk_droid_skeletons(devs, build, method)
1228 5
    with concurrent.futures.ThreadPoolExecutor(max_workers=len(skels)) as xec:
1229 5
        results = droid_scanner_worker(xec, skels, session)
1230 5
    return results if results else None
1231
1232
1233 5
def droid_scanner_worker(xec, skels, session=None):
1234
    """
1235
    Worker to check for Android autoloaders.
1236
1237
    :param xec: ThreadPoolExecutor instance.
1238
    :type xec: concurrent.futures.ThreadPoolExecutor
1239
1240
    :param skels: List of skeleton formats.
1241
    :type skels: list(str)
1242
1243
    :param session: Requests session object, default is created on the fly.
1244
    :type session: requests.Session()
1245
    """
1246 5
    results = []
1247 5
    for skel in skels:
1248 5
        avail = xec.submit(availability, skel, session)
1249 5
        if avail.result():
1250 5
            results.append(skel)
1251 5
    return results
1252
1253
1254 5
def chunker(iterable, inc):
1255
    """
1256
    Convert an iterable into a list of inc sized lists.
1257
1258
    :param iterable: Iterable to chunk.
1259
    :type iterable: list/tuple/string
1260
1261
    :param inc: Increment; how big each chunk is.
1262
    :type inc: int
1263
    """
1264 5
    chunks = [iterable[x:x+inc] for x in range(0, len(iterable), inc)]
1265 5
    return chunks
1266
1267
1268 5
def unicode_filter(intext):
1269
    """
1270
    Remove Unicode crap.
1271
1272
    :param intext: Text to filter.
1273
    :type intext: str
1274
    """
1275 5
    return intext.replace("\u2013", "").strip()
1276
1277
1278 5
def table_header_filter(ptag):
1279
    """
1280
    Validate p tag, to see if it's relevant.
1281
1282
    :param ptag: P tag.
1283
    :type ptag: bs4.element.Tag
1284
    """
1285 5
    valid = ptag.find("b") and "BlackBerry" in ptag.text and not "experts" in ptag.text
1286 5
    return valid
1287
1288
1289 5
def table_headers(pees):
1290
    """
1291
    Generate table headers from list of p tags.
1292
1293
    :param pees: List of p tags.
1294
    :type pees: list(bs4.element.Tag)
1295
    """
1296 5
    bolds = [x.text for x in pees if table_header_filter(x)]
1297 5
    return bolds
1298
1299
1300 5
@pem_wrapper
1301 5
def loader_page_scraper(session=None):
1302
    """
1303
    Return scraped autoloader pages.
1304
1305
    :param session: Requests session object, default is created on the fly.
1306
    :type session: requests.Session()
1307
    """
1308 5
    session = generic_session(session)
1309 5
    loader_page_scraper_og(session)
1310 5
    loader_page_scraper_bbm(session)
1311
1312
1313 5
def loader_page_scraper_og(session=None):
1314
    """
1315
    Return scraped autoloader page, original site.
1316
1317
    :param session: Requests session object, default is created on the fly.
1318
    :type session: requests.Session()
1319
    """
1320 5
    url = "https://ca.blackberry.com/support/smartphones/Android-OS-Reload.html"
1321 5
    soup = generic_soup_parser(url, session)
1322 5
    tables = soup.find_all("table")
1323 5
    headers = table_headers(soup.find_all("p"))
1324 5
    for idx, table in enumerate(tables):
1325 5
        loader_page_chunker_og(idx, table, headers)
1326
1327
1328 5
def loader_page_scraper_bbm(session=None):
1329
    """
1330
    Return scraped autoloader page, new site.
1331
1332
    :param session: Requests session object, default is created on the fly.
1333
    :type session: requests.Session()
1334
    """
1335 5
    url = "https://www.blackberrymobile.com/support/reload-software/"
1336 5
    soup = generic_soup_parser(url, session)
1337 5
    ulls = soup.find_all("ul", {"class": re.compile("list-two special-.")})[1:]
1338 5
    print("~~~BlackBerry KEYone~~~")
1339 5
    for ull in ulls:
1340 5
        loader_page_chunker_bbm(ull)
1341
1342
1343 5
def loader_page_chunker_og(idx, table, headers):
1344
    """
1345
    Given a loader page table, chunk it into lists of table cells.
1346
1347
    :param idx: Index of enumerating tables.
1348
    :type idx: int
1349
1350
    :param table: HTML table tag.
1351
    :type table: bs4.element.Tag
1352
1353
    :param headers: List of table headers.
1354
    :type headers: list(str)
1355
    """
1356 5
    print("~~~{0}~~~".format(headers[idx]))
1357 5
    chunks = chunker(table.find_all("td"), 4)
1358 5
    for chunk in chunks:
1359 5
        loader_page_printer(chunk)
1360 5
    print(" ")
1361
1362
1363 5
def loader_page_chunker_bbm(ull):
1364
    """
1365
    Given a loader page list, chunk it into lists of list items.
1366
1367
    :param ull: HTML unordered list tag.
1368
    :type ull: bs4.element.Tag
1369
    """
1370 5
    chunks = chunker(ull.find_all("li"), 3)
1371 5
    for chunk in chunks:
1372 5
        loader_page_printer(chunk)
1373
1374
1375 5
def loader_page_printer(chunk):
1376
    """
1377
    Print individual cell texts given a list of table cells.
1378
1379
    :param chunk: List of td tags.
1380
    :type chunk: list(bs4.element.Tag)
1381
    """
1382 5
    key = unicode_filter(chunk[0].text)
1383 5
    ver = unicode_filter(chunk[1].text)
1384 5
    link = unicode_filter(chunk[2].find("a")["href"])
1385 5
    print("{0}\n    {1}: {2}".format(key, ver, link))
1386
1387
1388 5
@pem_wrapper
1389 5
def base_metadata(url, session=None):
1390
    """
1391
    Get BBNDK metadata, base function.
1392
1393
    :param url: URL to check.
1394
    :type url: str
1395
1396
    :param session: Requests session object, default is created on the fly.
1397
    :type session: requests.Session()
1398
    """
1399 5
    session = generic_session(session)
1400 5
    req = session.get(url)
1401 5
    data = req.content
1402 5
    entries = data.split(b"\n")
1403 5
    metadata = [entry.split(b",")[1].decode("utf-8") for entry in entries if entry]
1404 5
    return metadata
1405
1406
1407 5
def ndk_metadata(session=None):
1408
    """
1409
    Get BBNDK target metadata.
1410
1411
    :param session: Requests session object, default is created on the fly.
1412
    :type session: requests.Session()
1413
    """
1414 5
    data = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/metadata", session)
1415 5
    metadata = [entry for entry in data if entry.startswith(("10.0", "10.1", "10.2"))]
1416 5
    return metadata
1417
1418
1419 5
def sim_metadata(session=None):
1420
    """
1421
    Get BBNDK simulator metadata.
1422
1423
    :param session: Requests session object, default is created on the fly.
1424
    :type session: requests.Session()
1425
    """
1426 5
    metadata = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/simulator/simulator_metadata", session)
1427 5
    return metadata
1428
1429
1430 5
def runtime_metadata(session=None):
1431
    """
1432
    Get BBNDK runtime metadata.
1433
1434
    :param session: Requests session object, default is created on the fly.
1435
    :type session: requests.Session()
1436
    """
1437 5
    metadata = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/runtime/runtime_metadata", session)
1438 5
    return metadata
1439
1440
1441 5
def series_generator(osversion):
1442
    """
1443
    Generate series/branch name from OS version.
1444
1445
    :param osversion: OS version.
1446
    :type osversion: str
1447
    """
1448 5
    splits = osversion.split(".")
1449 5
    return "BB{0}_{1}_{2}".format(*splits[0:3])
1450
1451
1452 5
@pem_wrapper
1453 5
def devalpha_urls(osversion, skel, session=None):
1454
    """
1455
    Check individual Dev Alpha autoloader URLs.
1456
1457
    :param osversion: OS version.
1458
    :type osversion: str
1459
1460
    :param skel: Individual skeleton format to try.
1461
    :type skel: str
1462
1463
    :param session: Requests session object, default is created on the fly.
1464
    :type session: requests.Session()
1465
    """
1466 5
    session = generic_session(session)
1467 5
    url = "http://downloads.blackberry.com/upr/developers/downloads/{0}{1}.exe".format(skel, osversion)
1468 5
    req = session.head(url)
1469 5
    if req.status_code == 200:
1470 5
        finals = (url, req.headers["content-length"])
1471
    else:
1472 5
        finals = ()
1473 5
    return finals
1474
1475
1476 5
def devalpha_urls_serieshandler(osversion, skeletons):
1477
    """
1478
    Process list of candidate Dev Alpha autoloader URLs.
1479
1480
    :param osversion: OS version.
1481
    :type osversion: str
1482
1483
    :param skeletons: List of skeleton formats to try.
1484
    :type skeletons: list
1485
    """
1486 5
    skels = skeletons
1487 5
    for idx, skel in enumerate(skeletons):
1488 5
        if "<SERIES>" in skel:
1489 5
            skels[idx] = skel.replace("<SERIES>", series_generator(osversion))
1490 5
    return skels
1491
1492
1493 5
def devalpha_urls_bulk(osversion, skeletons, xec, session=None):
1494
    """
1495
    Construct list of valid Dev Alpha autoloader URLs.
1496
1497
    :param osversion: OS version.
1498
    :type osversion: str
1499
1500
    :param skeletons: List of skeleton formats to try.
1501
    :type skeletons: list
1502
1503
    :param xec: ThreadPoolExecutor instance.
1504
    :type xec: concurrent.futures.ThreadPoolExecutor
1505
1506
    :param session: Requests session object, default is created on the fly.
1507
    :type session: requests.Session()
1508
    """
1509 5
    finals = {}
1510 5
    skels = devalpha_urls_serieshandler(osversion, skeletons)
1511 5
    for skel in skels:
1512 5
        final = xec.submit(devalpha_urls, osversion, skel, session).result()
1513 5
        if final:
1514 5
            finals[final[0]] = final[1]
1515 5
    return finals
1516
1517
1518 5
def devalpha_urls_bootstrap(osversion, skeletons, session=None):
1519
    """
1520
    Get list of valid Dev Alpha autoloader URLs.
1521
1522
    :param osversion: OS version.
1523
    :type osversion: str
1524
1525
    :param skeletons: List of skeleton formats to try.
1526
    :type skeletons: list
1527
1528
    :param session: Requests session object, default is created on the fly.
1529
    :type session: requests.Session()
1530
    """
1531 5
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as xec:
1532 5
        try:
1533 5
            return devalpha_urls_bulk(osversion, skeletons, xec, session)
1534 5
        except KeyboardInterrupt:
1535 5
            xec.shutdown(wait=False)
1536
1537
1538 5
def dev_dupe_dicter(finals):
1539
    """
1540
    Prepare dictionary to clean duplicate autoloaders.
1541
1542
    :param finals: Dict of URL:content-length pairs.
1543
    :type finals: dict(str: str)
1544
    """
1545 5
    revo = {}
1546 5
    for key, val in finals.items():
1547 5
        revo.setdefault(val, set()).add(key)
1548 5
    return revo
1549
1550
1551 5
def dev_dupe_remover(finals, dupelist):
1552
    """
1553
    Filter dictionary of autoloader entries.
1554
1555
    :param finals: Dict of URL:content-length pairs.
1556
    :type finals: dict(str: str)
1557
1558
    :param dupelist: List of duplicate URLs.
1559
    :type duplist: list(str)
1560
    """
1561 5
    for dupe in dupelist:
1562 5
        for entry in dupe:
1563 5
            if "DevAlpha" in entry:
1564 5
                del finals[entry]
1565 5
    return finals
1566
1567
1568 5
def dev_dupe_cleaner(finals):
1569
    """
1570
    Clean duplicate autoloader entries.
1571
1572
    :param finals: Dict of URL:content-length pairs.
1573
    :type finals: dict(str: str)
1574
    """
1575 5
    revo = dev_dupe_dicter(finals)
1576 5
    dupelist = [val for key, val in revo.items() if len(val) > 1]
1577 5
    finals = dev_dupe_remover(finals, dupelist)
1578
    return finals
1579