Last active
May 10, 2023 20:38
-
-
Save WitherOrNot/0531a879b55ffeb0d36145936b5f80e2 to your computer and use it in GitHub Desktop.
office click-to-run (c2r) downloader
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from bs4 import BeautifulSoup | |
from requests import head, get | |
from os import remove | |
from subprocess import run | |
from argparse import ArgumentParser | |
from platform import architecture | |
from shutil import copy | |
ROOT = "https://officecdn.microsoft.com/pr/{ffn}" | |
IMG_URLS = [ | |
'/media/{lang}/Access2019Retail.img', | |
'/media/{lang}/Access2021Retail.img', | |
'/media/{lang}/AccessRuntime2019Retail.img', | |
'/media/{lang}/Excel2019Retail.img', | |
'/media/{lang}/Excel2021Retail.img', | |
'/media/{lang}/HomeBusiness2019Retail.img', | |
'/media/{lang}/HomeBusiness2021Retail.img', | |
'/media/{lang}/HomeStudent2019Retail.img', | |
'/media/{lang}/HomeStudent2021Retail.img', | |
'/media/{lang}/O365BusinessRetail.img', | |
'/media/{lang}/O365HomePremRetail.img', | |
'/media/{lang}/O365ProPlusRetail.img', | |
'/media/{lang}/OneNote2021Retail.img', | |
'/media/{lang}/Outlook2019Retail.img', | |
'/media/{lang}/Outlook2021Retail.img', | |
'/media/{lang}/Personal2019Retail.img', | |
'/media/{lang}/Personal2021Retail.img', | |
'/media/{lang}/PowerPoint2019Retail.img', | |
'/media/{lang}/PowerPoint2021Retail.img', | |
'/media/{lang}/Professional2019Retail.img', | |
'/media/{lang}/Professional2021Retail.img', | |
'/media/{lang}/ProjectPro2019Retail.img', | |
'/media/{lang}/ProjectPro2021Retail.img', | |
'/media/{lang}/ProjectStd2019Retail.img', | |
'/media/{lang}/ProjectStd2021Retail.img', | |
'/media/{lang}/ProPlus2019Retail.img', | |
'/media/{lang}/ProPlus2021Retail.img', | |
'/media/{lang}/Publisher2019Retail.img', | |
'/media/{lang}/Publisher2021Retail.img', | |
'/media/{lang}/SkypeforBusiness2019Retail.img', | |
'/media/{lang}/SkypeforBusiness2021Retail.img', | |
'/media/{lang}/SkypeforBusinessEntry2019Retail.img', | |
'/media/{lang}/Standard2019Retail.img', | |
'/media/{lang}/Standard2021Retail.img', | |
'/media/{lang}/VisioPro2019Retail.img', | |
'/media/{lang}/VisioPro2021Retail.img', | |
'/media/{lang}/VisioStd2019Retail.img', | |
'/media/{lang}/VisioStd2021Retail.img', | |
'/media/{lang}/Word2019Retail.img', | |
'/media/{lang}/Word2021Retail.img' | |
] | |
def gen_url(ffn, arch, lang, path, version=""): | |
return (ROOT + path).format(ffn=ffn, arch=arch, lang=lang, version=version) | |
def check_file(ffn, arch, lang, path, version=""): | |
return head(gen_url(ffn, arch, lang, path, version)).status_code == 200 | |
def api_get(url): | |
return get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"}) | |
def get_channels(): | |
data = api_get("https://config.office.com/api/filelist/channels").json() | |
channels = {} | |
for channel in data: | |
ffn = channel["baseUrl"].split("/")[-1] | |
version = api_get(f"https://mrodevicemgr.officeapps.live.com/mrodevicemgrsvc/api/v2/C2RReleaseData/{ffn}").json()["AvailableBuild"] | |
channels[channel["name"]] = (ffn, version) | |
return channels | |
def scan_imgs(ffn, arch, lang): | |
for path in IMG_URLS: | |
if check_file(ffn, arch, lang, path): | |
print(gen_url(ffn, arch, lang, path)) | |
def download_c2r(channel, version, arch, langs, root_dir): | |
url = f"https://config.office.com/api/filelist?Channel={channel}&Arch={arch}&Version={version}&lid=x-none&" + "&".join(["lid=" + lang for lang in langs]) | |
filedata = api_get(url).json() | |
with open("c2r.tmp", "w") as f: | |
for fentry in filedata["files"]: | |
url = fentry["url"] | |
dir = root_dir + fentry["relativePath"] | |
name = fentry["name"] | |
f.write(f"{url}\n\tout={name}\n\tdir={dir}\n") | |
url = gen_url("wsus", "", "", "/setup.exe") | |
f.write(f"{url}\n\tdir={root_dir}\n") | |
run(["aria2c", "--console-log-level=error", "--summary-interval=0", "--download-result=hide", "-x", "16", "-j", "16", "-i", "c2r.tmp"]) | |
print() | |
remove("c2r.tmp") | |
def parse_config_xml(xml): | |
soup = BeautifulSoup(xml, "lxml") | |
langs = list(set(x.attrs["id"] for x in soup.find_all("language"))) | |
channel = soup.find("add").attrs["channel"] | |
arch = soup.find("add").attrs["officeclientedition"] | |
return channel, arch, langs | |
if __name__ == "__main__": | |
parser = ArgumentParser() | |
parser.add_argument("mode", help="config (download from configuration xml), img (get img installer urls), or channels (list available channels)") | |
parser.add_argument("--configuration", "-f", help="configuration xml file for download, default Configuration.xml", default="Configuration.xml") | |
parser.add_argument("--directory", "-d", help="directory to download to in config mode", default=None) | |
parser.add_argument("--channel", "-c", help="channel for img mode, default is Current", default="Current") | |
parser.add_argument("--arch", "-a", help="architecture for img mode, default is system architecture", default=architecture()[0][:2]) | |
parser.add_argument("--language", "-l", help="language for img mode, default is en-us", default="en-us") | |
args = parser.parse_args() | |
channels = get_channels() | |
if args.mode == "img": | |
print(f"Scanning IMG files for {args.arch}-bit {args.channel} channel in {args.language} locale:") | |
ffn, version = channels[args.channel] | |
scan_imgs(ffn, args.arch, args.language) | |
elif args.mode == "config": | |
with open(args.configuration, "r") as f: | |
channel, arch, langs = parse_config_xml(f.read()) | |
ffn, version = channels[channel] | |
print(f"Downloading C2R setup files for {arch}-bit {channel} channel in locale(s) {', '.join(langs)}:") | |
if args.directory is None: | |
directory = f"{channel}_{version}_{'_'.join(langs)}_x{arch}" | |
else: | |
directory = args.directory | |
download_c2r(channel, version, arch, langs, directory) | |
copy(args.configuration, directory) | |
elif args.mode == "channels": | |
print("Available channels:") | |
for channel in channels: | |
ffn, version = channels[channel] | |
print(f"{channel} (FFN {ffn}): version {version}") | |
else: | |
parser.print_help() | |
exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment