diff --git a/.gitignore b/.gitignore index 946d8aa5..e53da6d8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ build/ .DS_Store # project specific -data/** metadata/** incomplete/** reject/** @@ -26,3 +25,8 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ + +# ignore all contents of data/ EXCEPT data/base_executables & contents of base_executables +data/** +!data/base_executables +!data/base_executables/** \ No newline at end of file diff --git a/app/config.py b/app/config.py index 308d158e..aca7b3fb 100644 --- a/app/config.py +++ b/app/config.py @@ -29,8 +29,8 @@ if not os.path.isdir(task_badupload_dir): os.makedirs(task_badupload_dir) dl_dir = os.path.join(data_dir, 'download') if not os.path.isdir(dl_dir): os.makedirs(dl_dir) -exe_dir = os.path.join(data_dir, 'exe') -if not os.path.isdir(exe_dir): os.makedirs(exe_dir) +base_exe_dir = os.path.join(data_dir, 'base_executables') +if not os.path.isdir(base_exe_dir): os.makedirs(base_exe_dir) survey_dir = os.path.join(data_dir, 'survey') if not os.path.isdir(survey_dir): os.makedirs(survey_dir) survey_incomplete_dir = os.path.join(survey_dir, 'incomplete') @@ -65,7 +65,8 @@ s_reject=survey_reject_dir, s_complete=survey_complete_dir, download=dl_dir, - exe=dl_dir, + base_exe=os.path.join(base_exe_dir, 'SUPREME.exe'), + base_app=os.path.join(base_exe_dir, 'SUPREME.app'), disallowed_agents=json.loads(cfg['FLASK']['DISALLOWED_AGENTS']), allowed_agents=json.loads(cfg['FLASK']['ALLOWED_AGENTS']), blocks=json.loads(cfg['SUPREME']['BLOCKS']), diff --git a/app/taskstart.py b/app/taskstart.py index b7a4befa..3a8639a3 100644 --- a/app/taskstart.py +++ b/app/taskstart.py @@ -12,7 +12,7 @@ from .io import write_metadata, initialize_taskdata from .config import CFG from .routing import routing -from .utils import make_download +from .utils import edit_exe_worker_id, edit_app_worker_id from hashlib import blake2b @@ -29,8 +29,8 @@ def taskstart(): supreme_subid = current_app.config['SUPREME_serializer'].dumps(h_workerId) win_dlpath = os.path.join(CFG['download'], 'win_' + str(session['subId']) + '.exe') mac_dlpath = os.path.join(CFG['download'], 'mac_' + str(session['subId']) + '.app') - make_download(supreme_subid, win_dlpath, 'windows') - make_download(supreme_subid, mac_dlpath, 'mac') + edit_exe_worker_id(exe_file_path=CFG['base_exe'], new_worker_id=supreme_subid, output_file_path=win_dlpath) + edit_app_worker_id(app_path=CFG['base_app'], new_worker_id=supreme_subid, output_app_path=mac_dlpath) session['dlready'] = True write_metadata(session, ['dlready'], 'a') initialize_taskdata(session) diff --git a/app/utils.py b/app/utils.py index b7d44fbf..27c36723 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,22 +1,26 @@ import random import string import copy +import logging +import os +import plistlib +import shutil +from typing import Optional +from pathlib import Path +from pefile import PE, DIRECTORY_ENTRY + def gen_code(N): """Generate random completion code.""" return ''.join(random.choices(string.ascii_lowercase + string.digits, k=N)) -def make_download(sid, dlpath, platform): - pass - - def pseudorandomize(inblocks, nreps, shuffle_blocks=True, nested_output=False): """ pseudorandomize the input blocks so that each task occurs once before any task is repeated and no tasks occur back to back. Will trigger an infinite loop if the number of blocks it too small to generate enough unique - orders to satisfy the shuffle blocks condition for the number of repititions + orders to satisfy the shuffle blocks condition for the number of repetitions requested. Parameters @@ -24,9 +28,9 @@ def pseudorandomize(inblocks, nreps, shuffle_blocks=True, nested_output=False): inblocks : list of strings or list of list of strings list of tasks to randomize nreps : int - number of repititions of each task + number of repetitions of each task shuffle blocks : bool - Should task order be shuffeled everytime they're repeated + Should task order be shuffled every time they're repeated nested_output : bool Should the output be nested (list of lists) @@ -54,3 +58,132 @@ def pseudorandomize(inblocks, nreps, shuffle_blocks=True, nested_output=False): for task in task_block: flat_blocks.append(task) return flat_blocks + + +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + + +def edit_app_worker_id(app_path: str, new_worker_id: str, output_app_path: Optional[str] = None) -> None: + """ + Modifies the 'WorkerID' field in the Info.plist of a macOS .app bundle. + + Args: + app_path (str): Path to the original .app bundle whose 'WorkerID' will be modified. + new_worker_id (str): New 'WorkerID' value to replace the existing one. + output_app_path (Optional[str]): Path to save the modified .app bundle. If not provided, + the original bundle will be overwritten. + + Raises: + FileNotFoundError: If the specified .app bundle or Info.plist file does not exist. + ValueError: If the Info.plist file cannot be loaded or parsed. + """ + + # Construct the path to the Info.plist file inside the original .app bundle + plist_path = Path(app_path) / 'Contents' / 'Info.plist' + + # Ensure the .app bundle and Info.plist exist + if not plist_path.exists(): + raise FileNotFoundError(f"The file {plist_path} does not exist.") + + try: + # If an output path is specified, copy the original .app bundle to the new location + if output_app_path: + output_path = Path(output_app_path) + if output_path.exists(): + raise FileExistsError( + f"The output path {output_app_path} already exists.") + shutil.copytree(app_path, output_app_path) + # Update plist path to the new location + plist_path = output_path / 'Contents' / 'Info.plist' + else: + logging.info( + "No output path specified. The original .app bundle will be modified.") + + # Load the plist file + with plist_path.open('rb') as plist_file: + plist_data = plistlib.load(plist_file) + + # Log current WorkerID, if present + current_worker_id = plist_data.get('WorkerID', None) + if current_worker_id: + logging.info(f"Current WorkerID: {current_worker_id}") + + # Update the WorkerID + plist_data['WorkerID'] = new_worker_id + + # Write the updated plist back + with plist_path.open('wb') as plist_file: + plistlib.dump(plist_data, plist_file) + + logging.info(f"Successfully updated WorkerID to {new_worker_id}") + + except Exception as e: + raise ValueError(f"Failed to modify the plist file: {e}") + + +def edit_exe_worker_id(exe_file_path: str, new_worker_id: str, output_file_path: Optional[str] = None) -> None: + """ + Modifies the 'WorkerID' field in the version information of an executable. + + Args: + exe_file_path (str): Path to the executable whose 'WorkerID' will be modified. + new_worker_id (str): New 'WorkerID' value to replace the existing one. + **Must** be less than or equal to the length of the current 'WorkerID' for a successful update. + output_file_path (str): **Optional**. Path where the modified executable will be saved. + If not provided, the original executable will be overwritten. + + Raises: + FileNotFoundError: If the specified executable does not exist. + ValueError: If the PE file cannot be loaded or parsed. + """ + + if not os.path.exists(exe_file_path): + raise FileNotFoundError(f"The file {exe_file_path} does not exist.") + + output_file_path = Path(output_file_path or exe_file_path) + + try: + pe: PE = PE(exe_file_path, fast_load=True) + pe.parse_data_directories( + directories=[DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']]) + except Exception as e: + raise ValueError(f"Failed to load or parse the PE file: {e}") + + # Access the WorkerID and update it if found + if hasattr(pe, 'FileInfo'): + for file_info in pe.FileInfo: + for info in file_info: + if info.name == 'StringFileInfo': + version_info_dict: dict = info.StringTable[0].entries + worker_id_bytes: bytes = version_info_dict.get( + b'WorkerID', None) + + if worker_id_bytes: + logging.info( + f"Found WorkerID: {worker_id_bytes.decode('utf-8')}") + + # Calculate the original size in bytes and the new value size + original_size: int = len(worker_id_bytes) + new_worker_id_bytes: bytes = new_worker_id.encode( + 'utf-8') + new_size: int = len(new_worker_id_bytes) + + if new_size <= original_size: + # Create the new value padded with null bytes up to the original size + padded_new_worker_id: bytes = new_worker_id_bytes + \ + b'\x00' * (original_size - new_size) + + # Update the value with the padded version + version_info_dict[b"WorkerID"] = padded_new_worker_id + + # Write the updated attribute to the output file + pe.write(output_file_path) + logging.info( + f"Successfully updated WorkerID to {new_worker_id}") + else: + logging.error( + f"Error: New value '{new_worker_id}' is larger than the existing space of {original_size} bytes.") + return + + logging.error(f"Error: WorkerID not found in {exe_file_path}") \ No newline at end of file diff --git a/data/base_executables/SUPREME.app/Contents/Info.plist b/data/base_executables/SUPREME.app/Contents/Info.plist new file mode 100644 index 00000000..9963fdeb --- /dev/null +++ b/data/base_executables/SUPREME.app/Contents/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleDisplayName + SUPREME + CFBundleExecutable + SUPREME + CFBundleIconFile + icon-windowed.icns + CFBundleIdentifier + uva.compmem.supreme + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + SUPREME + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + 1.0.0 + LSArchitecturePriority + + x86_64 + arm64 + + NSHighResolutionCapable + True + WorkerID + "------------------------".--------------------------- + + diff --git a/data/base_executables/SUPREME.app/Contents/MacOS/SUPREME b/data/base_executables/SUPREME.app/Contents/MacOS/SUPREME new file mode 100755 index 00000000..5320f7f2 Binary files /dev/null and b/data/base_executables/SUPREME.app/Contents/MacOS/SUPREME differ diff --git a/data/base_executables/SUPREME.app/Contents/Resources/icon-windowed.icns b/data/base_executables/SUPREME.app/Contents/Resources/icon-windowed.icns new file mode 100644 index 00000000..954a9a05 Binary files /dev/null and b/data/base_executables/SUPREME.app/Contents/Resources/icon-windowed.icns differ diff --git a/data/base_executables/SUPREME.app/Contents/_CodeSignature/CodeResources b/data/base_executables/SUPREME.app/Contents/_CodeSignature/CodeResources new file mode 100644 index 00000000..ae69c027 --- /dev/null +++ b/data/base_executables/SUPREME.app/Contents/_CodeSignature/CodeResources @@ -0,0 +1,128 @@ + + + + + files + + Resources/icon-windowed.icns + + eEHOuYpZLB0vKGVIWGZOh5rH8+o= + + + files2 + + Resources/icon-windowed.icns + + hash2 + + uQo7VuWRab4Phv4EEGmfQsyqFqDIXZgO8OtgaAMvCzY= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/data/base_executables/SUPREME.exe b/data/base_executables/SUPREME.exe new file mode 100644 index 00000000..b4f4eb28 Binary files /dev/null and b/data/base_executables/SUPREME.exe differ diff --git a/requirements.txt b/requirements.txt index 796dd39f..498460bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ numpy +pefile flask>=2.0 gunicorn pytest