From 62f619da99046fcd9c20a030c53cff402dff8637 Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Tue, 11 Apr 2023 08:42:21 -0600 Subject: [PATCH 1/6] network_handler.py: Add post wrapping --- resources/network_handler.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/resources/network_handler.py b/resources/network_handler.py index 26e880417..f89bcc91d 100644 --- a/resources/network_handler.py +++ b/resources/network_handler.py @@ -109,6 +109,35 @@ class NetworkUtilities: return result + def post(self, url: str, **kwargs) -> requests.Response: + """ + Wrapper for requests's post method + Implement additional error handling + + Parameters: + url (str): URL to post + **kwargs: Additional parameters for requests.post + + Returns: + requests.Response: Response object from requests.post + """ + + result: requests.Response = None + + try: + result = SESSION.post(url, **kwargs) + except ( + requests.exceptions.Timeout, + requests.exceptions.TooManyRedirects, + requests.exceptions.ConnectionError, + requests.exceptions.HTTPError + ) as error: + logging.warn(f"Error calling requests.post: {error}") + # Return empty response object + return requests.Response() + + return result + class DownloadObject: """ From 52211def511cba204e6e605dd3a9dc50fddc891f Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Tue, 11 Apr 2023 13:28:33 -0600 Subject: [PATCH 2/6] Test build of analytics --- .github/workflows/build-app-wxpython.yml | 4 +- Build-Binary.command | 70 ++++++++++++++++++ resources/analytics_handler.py | 94 ++++++++++++++++++++++++ resources/device_probe.py | 4 + resources/main.py | 5 +- 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 resources/analytics_handler.py diff --git a/.github/workflows/build-app-wxpython.yml b/.github/workflows/build-app-wxpython.yml index a803e4496..000198293 100644 --- a/.github/workflows/build-app-wxpython.yml +++ b/.github/workflows/build-app-wxpython.yml @@ -18,10 +18,12 @@ jobs: commitdate: ${{ github.event.head_commit.timestamp }}${{ github.event.release.published_at }} MAC_NOTARIZATION_USERNAME: ${{ secrets.MAC_NOTARIZATION_USERNAME }} MAC_NOTARIZATION_PASSWORD: ${{ secrets.MAC_NOTARIZATION_PASSWORD }} + ANALYTICS_KEY: ${{ secrets.ANALYTICS_KEY }} + ANALYTICS_SITE: ${{ secrets.ANALYTICS_SITE }} steps: - uses: actions/checkout@v3 - - run: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3 Build-Binary.command --reset_binaries --branch "${{ env.branch }}" --commit "${{ env.commiturl }}" --commit_date "${{ env.commitdate }}" + - run: /Library/Frameworks/Python.framework/Versions/3.10/bin/python3 Build-Binary.command --reset_binaries --branch "${{ env.branch }}" --commit "${{ env.commiturl }}" --commit_date "${{ env.commitdate }}" --key "${{ env.ANALYTICS_KEY }}" --site "${{ env.ANALYTICS_SITE }}" - run: 'codesign -s "Developer ID Application: Mykola Grymalyuk (S74BDJXQMD)" -v --force --deep --timestamp --entitlements ./payloads/entitlements.plist -o runtime "dist/OpenCore-Patcher.app"' - run: cd dist; ditto -c -k --sequesterRsrc --keepParent OpenCore-Patcher.app ../OpenCore-Patcher-wxPython.app.zip - run: xcrun altool --notarize-app --primary-bundle-id "com.dortania.opencore-legacy-patcher" --username "${{ env.MAC_NOTARIZATION_USERNAME }}" --password "${{ env.MAC_NOTARIZATION_PASSWORD }}" --file OpenCore-Patcher-wxPython.app.zip diff --git a/Build-Binary.command b/Build-Binary.command index 92c0e055f..9e23a7ee9 100755 --- a/Build-Binary.command +++ b/Build-Binary.command @@ -61,6 +61,8 @@ class CreateBinary: parser.add_argument('--commit', type=str, help='Git commit URL') parser.add_argument('--commit_date', type=str, help='Git commit date') parser.add_argument('--reset_binaries', action='store_true', help='Force redownload and imaging of payloads') + parser.add_argument('--key', type=str, help='Developer key for signing') + parser.add_argument('--site', type=str, help='Path to server') args = parser.parse_args() return args @@ -132,17 +134,85 @@ class CreateBinary: print(rm_output.stderr.decode('utf-8')) raise Exception("Remove failed") + self._embed_key() print("- Building GUI binary...") build_args = [self.pyinstaller_path, "./OpenCore-Patcher-GUI.spec", "--noconfirm"] build_result = subprocess.run(build_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + self._strip_key() + if build_result.returncode != 0: print("- Build failed") print(build_result.stderr.decode('utf-8')) raise Exception("Build failed") + + + def _embed_key(self): + """ + Embed developer key into binary + """ + + if not self.args.key: + print("- No developer key provided, skipping...") + return + if not self.args.site: + print("- No site provided, skipping...") + return + + print("- Embedding developer key...") + if not Path("./resources/analytics_handler.py").exists(): + print("- analytics_handler.py not found") + return + + lines = [] + with open("./resources/analytics_handler.py", "r") as f: + lines = f.readlines() + + for i, line in enumerate(lines): + if line.startswith("SITE_KEY: str = "): + lines[i] = f"SITE_KEY: str = \"{self.args.key}\"\n" + elif line.startswith("ANALYTICS_SERVER: str = "): + lines[i] = f"ANALYTICS_SERVER: str = \"{self.args.site}\"\n" + + with open("./resources/analytics_handler.py", "w") as f: + f.writelines(lines) + + + def _strip_key(self): + """ + Strip developer key from binary + """ + + if not self.args.key: + print("- No developer key provided, skipping...") + return + if not self.args.site: + print("- No site provided, skipping...") + return + + print("- Stripping developer key...") + if not Path("./resources/analytics_handler.py").exists(): + print("- analytics_handler.py not found") + return + + lines = [] + with open("./resources/analytics_handler.py", "r") as f: + lines = f.readlines() + + for i, line in enumerate(lines): + if line.startswith("SITE_KEY: str = "): + lines[i] = f"SITE_KEY: str = \"\"\n" + elif line.startswith("ANALYTICS_SERVER: str = "): + lines[i] = f"ANALYTICS_SERVER: str = \"\"\n" + + with open("./resources/analytics_handler.py", "w") as f: + f.writelines(lines) + + def _delete_extra_binaries(self): """ Delete extra binaries from payloads directory diff --git a/resources/analytics_handler.py b/resources/analytics_handler.py new file mode 100644 index 000000000..dc01e5a9b --- /dev/null +++ b/resources/analytics_handler.py @@ -0,0 +1,94 @@ +import datetime +import plistlib +from pathlib import Path +import json + +from resources import network_handler, constants + + +DATE_FORMAT: str = "%Y-%m-%d %H-%M-%S" +ANALYTICS_SERVER: str = "" +SITE_KEY: str = "" + +VALID_ENTRIES: dict = { + 'KEY': str, # Prevent abuse (embedded at compile time) + 'UNIQUE_IDENTITY': str, # Host's UUID as SHA1 hash + 'APPLICATION_NAME': str, # ex. OpenCore Legacy Patcher + 'APPLICATION_VERSION': str, # ex. 0.2.0 + 'OS_VERSION': str, # ex. 10.15.7 + 'MODEL': str, # ex. MacBookPro11,5 + 'GPUS': list, # ex. ['Intel Iris Pro', 'AMD Radeon R9 M370X'] + 'FIRMWARE': str, # ex. APPLE + 'LOCATION': str, # ex. 'US' (just broad region, don't need to be specific) + 'TIMESTAMP': datetime.datetime, # ex. 2021-09-01-12-00-00 +} + + +class Analytics: + + def __init__(self, global_constants: constants.Constants) -> None: + self.constants: constants.Constants = global_constants + + self._generate_base_data() + self._post_data() + + + def _get_country(self) -> str: + # Get approximate country from .GlobalPreferences.plist + path = "/Library/Preferences/.GlobalPreferences.plist" + if not Path(path).exists(): + return "US" + + try: + result = plistlib.load(Path(path).open("rb")) + except: + return "US" + + if "Country" not in result: + return "US" + + return result["Country"] + + + def _generate_base_data(self) -> None: + + self.unique_identity = str(self.constants.computer.uuid_sha1) + self.application = str("OpenCore Legacy Patcher") + self.version = str(self.constants.patcher_version) + self.os = str( self.constants.detected_os_version) + self.model = str(self.constants.computer.real_model) + self.gpus = [] + + self.firmware = str(self.constants.computer.firmware_vendor) + self.location = str(self._get_country()) + + for gpu in self.constants.computer.gpus: + self.gpus.append(str(gpu.arch)) + + self.data = { + 'KEY': SITE_KEY, + 'UNIQUE_IDENTITY': self.unique_identity, + 'APPLICATION_NAME': self.application, + 'APPLICATION_VERSION': self.version, + 'OS_VERSION': self.os, + 'MODEL': self.model, + 'GPUS': self.gpus, + 'FIRMWARE': self.firmware, + 'LOCATION': self.location, + 'TIMESTAMP': str(datetime.datetime.now().strftime(DATE_FORMAT)), + } + + # convert to JSON: + self.data = json.dumps(self.data) + + + def _post_data(self) -> None: + # Post data to analytics server + if ANALYTICS_SERVER == "": + return + if SITE_KEY == "": + return + network_handler.NetworkUtilities().post(ANALYTICS_SERVER, json = self.data) + + + diff --git a/resources/device_probe.py b/resources/device_probe.py index 5506289af..11d4c72e7 100644 --- a/resources/device_probe.py +++ b/resources/device_probe.py @@ -6,6 +6,7 @@ import enum import itertools import subprocess import plistlib +import hashlib from pathlib import Path from dataclasses import dataclass, field from typing import Any, ClassVar, Optional, Type, Union @@ -491,6 +492,7 @@ class Computer: reported_model: Optional[str] = None reported_board_id: Optional[str] = None build_model: Optional[str] = None + uuid_sha1: Optional[str] = None gpus: list[GPU] = field(default_factory=list) igpu: Optional[GPU] = None # Shortcut for IGPU dgpu: Optional[GPU] = None # Shortcut for GFX0 @@ -719,6 +721,8 @@ class Computer: else: board = "board-id" self.reported_board_id = ioreg.corefoundation_to_native(ioreg.IORegistryEntryCreateCFProperty(entry, board, ioreg.kCFAllocatorDefault, ioreg.kNilOptions)).strip(b"\0").decode() # type: ignore + self.uuid_sha1 = ioreg.corefoundation_to_native(ioreg.IORegistryEntryCreateCFProperty(entry, "IOPlatformUUID", ioreg.kCFAllocatorDefault, ioreg.kNilOptions)) # type: ignore + self.uuid_sha1 = hashlib.sha1(self.uuid_sha1.encode()).hexdigest() ioreg.IOObjectRelease(entry) # Real model diff --git a/resources/main.py b/resources/main.py index fd937d8ec..dcb7c0da7 100644 --- a/resources/main.py +++ b/resources/main.py @@ -16,7 +16,8 @@ from resources import ( arguments, reroute_payloads, commit_info, - logging_handler + logging_handler, + analytics_handler, ) @@ -110,4 +111,6 @@ class OpenCoreLegacyPatcher: while self.constants.unpack_thread.is_alive(): time.sleep(0.1) + threading.Thread(target=analytics_handler.Analytics, args=(self.constants,)).start() + arguments.arguments(self.constants) From b34eaccd35fcddc0252c446edb27b3742038423e Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Tue, 11 Apr 2023 13:54:45 -0600 Subject: [PATCH 3/6] main.py: move threading --- resources/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/main.py b/resources/main.py index dcb7c0da7..75daeb699 100644 --- a/resources/main.py +++ b/resources/main.py @@ -90,6 +90,7 @@ class OpenCoreLegacyPatcher: # Generate defaults defaults.GenerateDefaults(self.computer.real_model, True, self.constants) + threading.Thread(target=analytics_handler.Analytics, args=(self.constants,)).start() if utilities.check_cli_args() is None: logging.info(f"- No arguments present, loading {'GUI' if self.constants.wxpython_variant is True else 'TUI'} mode") @@ -111,6 +112,4 @@ class OpenCoreLegacyPatcher: while self.constants.unpack_thread.is_alive(): time.sleep(0.1) - threading.Thread(target=analytics_handler.Analytics, args=(self.constants,)).start() - arguments.arguments(self.constants) From 4c19f51a04c0d00f5046629801327420b89b7cda Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Tue, 11 Apr 2023 19:08:52 -0600 Subject: [PATCH 4/6] Analytics: allow opt-out --- resources/analytics_handler.py | 5 ++++- resources/gui/gui_main.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/resources/analytics_handler.py b/resources/analytics_handler.py index dc01e5a9b..1ad95a8b8 100644 --- a/resources/analytics_handler.py +++ b/resources/analytics_handler.py @@ -3,7 +3,7 @@ import plistlib from pathlib import Path import json -from resources import network_handler, constants +from resources import network_handler, constants, global_settings DATE_FORMAT: str = "%Y-%m-%d %H-%M-%S" @@ -29,6 +29,9 @@ class Analytics: def __init__(self, global_constants: constants.Constants) -> None: self.constants: constants.Constants = global_constants + if global_settings.GlobalEnviromentSettings().read_property("DisableAnalytics") is True: + return + self._generate_base_data() self._post_data() diff --git a/resources/gui/gui_main.py b/resources/gui/gui_main.py index ca41b8fa9..69f8ff17e 100644 --- a/resources/gui/gui_main.py +++ b/resources/gui/gui_main.py @@ -2980,12 +2980,24 @@ class wx_python_gui: self.delete_unused_kdks_checkbox.GetPosition().y + self.delete_unused_kdks_checkbox.GetSize().height)) self.set_ignore_app_updates_checkbox.SetToolTip(wx.ToolTip("This will set whether OpenCore will ignore App Updates on launch.\nEnable this option if you do not want to be prompted for App Updates")) + # Set Disable Analytics + res = global_settings.GlobalEnviromentSettings().read_property("DisableAnalytics") + if res is None: + res = False + self.set_disable_analytics_checkbox = wx.CheckBox(self.frame_modal, label="Disable Analytics") + self.set_disable_analytics_checkbox.SetValue(res) + self.set_disable_analytics_checkbox.Bind(wx.EVT_CHECKBOX, self.set_disable_analytics_click) + self.set_disable_analytics_checkbox.SetPosition(wx.Point( + self.set_ignore_app_updates_checkbox.GetPosition().x, + self.set_ignore_app_updates_checkbox.GetPosition().y + self.set_ignore_app_updates_checkbox.GetSize().height)) + self.set_disable_analytics_checkbox.SetToolTip(wx.ToolTip("Sets whether anonymized analytics are sent to the Dortania team.\nThis is used to help improve the application and is completely optional.")) + # Button: Developer Debug Info self.debug_button = wx.Button(self.frame_modal, label="Developer Debug Info") self.debug_button.Bind(wx.EVT_BUTTON, self.additional_info_menu) self.debug_button.SetPosition(wx.Point( - self.set_ignore_app_updates_checkbox.GetPosition().x, - self.set_ignore_app_updates_checkbox.GetPosition().y + self.set_ignore_app_updates_checkbox.GetSize().height + 5)) + self.set_disable_analytics_checkbox.GetPosition().x, + self.set_disable_analytics_checkbox.GetPosition().y + self.set_disable_analytics_checkbox.GetSize().height + 5)) self.debug_button.Center(wx.HORIZONTAL) # Button: return to main menu @@ -3038,6 +3050,9 @@ class wx_python_gui: else: global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", False) + def set_disable_analytics_click(self, event): + global_settings.GlobalEnviromentSettings().write_property("DisableAnalytics", self.set_disable_analytics_checkbox.GetValue()) + def firewire_click(self, event=None): if self.firewire_boot_checkbox.GetValue(): logging.info("Firewire Enabled") From 939d3a36a462941a26518ba3405c5b8832d4da55 Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Tue, 11 Apr 2023 19:58:25 -0600 Subject: [PATCH 5/6] Adjust wording --- CHANGELOG.md | 4 ++++ resources/analytics_handler.py | 2 +- resources/gui/gui_main.py | 9 ++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82ffbcd20..32b88aef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # OpenCore Legacy Patcher changelog ## 0.6.4 +- Backend changes: + - Implement new analytics_handler.py module + - Adds support for anonymous analytics including host info (and crash reports in the future) + - Can be disabled via GUI or `defaults write com.dortania.opencore-legacy-patcher DisableCrashAndAnalyticsReporting -bool true` ## 0.6.3 - Update non-Metal Binaries: diff --git a/resources/analytics_handler.py b/resources/analytics_handler.py index 1ad95a8b8..64e276966 100644 --- a/resources/analytics_handler.py +++ b/resources/analytics_handler.py @@ -29,7 +29,7 @@ class Analytics: def __init__(self, global_constants: constants.Constants) -> None: self.constants: constants.Constants = global_constants - if global_settings.GlobalEnviromentSettings().read_property("DisableAnalytics") is True: + if global_settings.GlobalEnviromentSettings().read_property("DisableCrashAndAnalyticsReporting") is True: return self._generate_base_data() diff --git a/resources/gui/gui_main.py b/resources/gui/gui_main.py index 69f8ff17e..6965e814f 100644 --- a/resources/gui/gui_main.py +++ b/resources/gui/gui_main.py @@ -2981,10 +2981,9 @@ class wx_python_gui: self.set_ignore_app_updates_checkbox.SetToolTip(wx.ToolTip("This will set whether OpenCore will ignore App Updates on launch.\nEnable this option if you do not want to be prompted for App Updates")) # Set Disable Analytics - res = global_settings.GlobalEnviromentSettings().read_property("DisableAnalytics") - if res is None: - res = False - self.set_disable_analytics_checkbox = wx.CheckBox(self.frame_modal, label="Disable Analytics") + res = global_settings.GlobalEnviromentSettings().read_property("DisableCrashAndAnalyticsReporting") + res = False if res is None else res + self.set_disable_analytics_checkbox = wx.CheckBox(self.frame_modal, label="Disable Crash/Analytics") self.set_disable_analytics_checkbox.SetValue(res) self.set_disable_analytics_checkbox.Bind(wx.EVT_CHECKBOX, self.set_disable_analytics_click) self.set_disable_analytics_checkbox.SetPosition(wx.Point( @@ -3051,7 +3050,7 @@ class wx_python_gui: global_settings.GlobalEnviromentSettings().write_property("IgnoreAppUpdates", False) def set_disable_analytics_click(self, event): - global_settings.GlobalEnviromentSettings().write_property("DisableAnalytics", self.set_disable_analytics_checkbox.GetValue()) + global_settings.GlobalEnviromentSettings().write_property("DisableCrashAndAnalyticsReporting", self.set_disable_analytics_checkbox.GetValue()) def firewire_click(self, event=None): if self.firewire_boot_checkbox.GetValue(): From ccbb5dafe5fdf0dce09f5737d65fc9784b8d629b Mon Sep 17 00:00:00 2001 From: Mykola Grymalyuk Date: Wed, 12 Apr 2023 16:31:04 -0600 Subject: [PATCH 6/6] Add PRIVACY.md --- PRIVACY.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 PRIVACY.md diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 000000000..cb8c811ea --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,25 @@ +# Privacy Policy + +OpenCore Legacy Patcher may collect pseudo-anonymized data about the host system and the OpenCore Legacy Patcher application. This data is used to improve the project and to help diagnose issues. The data collected is as follows: + +* System's UUID as a SHA1 hash + * This is used to identify the system and to prevent duplicate reports + * Cannot be used to identify the system without the user providing the UUID +* Application name and version +* System's OS version +* System's model name, GPUs present and firmware vendor + * May include more hardware information in the future (ex. CPU, WiFi, etc) +* General country code of system's reported region + * ex. `US`, `CA`, etc + +Identifiable data such as IP addresses, MAC addresses, serial numbers, etc. are not collected. + +In the future, crash logs may also be collected to help with diagnosing issues. +---------- + +Users who wish to opt-out can do so either via the application's preferences or via the following command: +``` +defaults write com.dortania.opencore-legacy-patcher DisableCrashAndAnalyticsReporting -bool true +``` + +To have your data removed, please contact us via our [Discord server](https://discord.gg/rqdPgH8xSN) and provide the UUID of your system. \ No newline at end of file