def fetch_entitlements_if_necessary(self): log.debug('Fetching origin entitlements') # Get the etag etag_path = get_data_path('origin-entitlements.etag') etag = '' if isfile(self.entitlements_path) and isfile(etag_path): with open(etag_path, mode='r') as file: etag = file.read() # Fetch entitlements if etag does not match url = 'https://raw.githubusercontent.com/acidicoala/origin-entitlements/master/entitlements.json' response = requests.get(url, headers={'If-None-Match': etag}) if response.status_code == 304: log.debug(f'Cached Origin entitlements have not changed') return if response.status_code != 200: log.error( f'Error while fetching entitlements: {response.status_code} - {response.text}' ) return # Cache entitlements with open(self.entitlements_path, 'w') as f: f.write(response.text) # Cache etag with open(etag_path, 'w') as f: f.write(response.headers['etag']) log.info('Origin entitlements were successfully fetched and cached')
def fetch_entitlements_if_necessary(self): log.debug('Fetching Origin entitlements') # Get the etag etag_path = get_data_path('origin-entitlements.etag') etag = '' if isfile(self.entitlements_path) and isfile(etag_path): with open(etag_path, mode='r') as file: etag = file.read() # Fetch entitlements if etag does not match url = 'https://raw.githubusercontent.com/acidicoala/public-entitlements/main/origin/v1/entitlements.json' try: response = requests.get(url, headers={'If-None-Match': etag}, timeout=10) except Exception as e: log.error(f"Failed to fetch origin entitlements. {str(e)}") return if response.status_code == 304: log.debug(f'Cached Origin entitlements have not changed') return if response.status_code != 200: log.error(f'Error while fetching entitlements: {response.status_code} - {response.text}') return try: index = 1000000 entitlements: List[dict] = json.loads(response.text) for entitlement in entitlements: entitlement.update({ "entitlementId": index, "lastModifiedDate": "2020-01-01T00:00Z", "entitlementSource": "ORIGIN-OIG", "grantDate": "2020-01-01T00:00:00Z", "suppressedBy": [], "version": 0, "isConsumable": False, "productCatalog": "OFB", "suppressedOffers": [], "originPermissions": "0", "useCount": 0, "projectId": "123456", "status": "ACTIVE" }) index += 1 except ValueError as e: log.error(f"Failed to decode entitlements from json. {str(e)}") return # Cache entitlements with open(self.entitlements_path, 'w') as f: f.write(json.dumps(entitlements, indent=4, ensure_ascii=False)) # Cache etag with open(etag_path, 'w') as f: f.write(response.headers['etag']) log.info('Origin entitlements were successfully fetched and cached')
class OriginAddon(BaseAddon): last_origin_pid = 0 injected_entitlements = [] api_host = r'api\d*\.origin\.com' entitlements_path = get_data_path('origin-entitlements.json') hosts = BaseAddon.get_hosts([api_host]) @log_exceptions def __init__(self): self.patch_origin_client() self.fetch_entitlements_if_necessary() self.read_entitlements_from_cache() @staticmethod @log_exceptions def request(flow: HTTPFlow): OriginAddon.block_telemetry(flow) @log_exceptions def response(self, flow: HTTPFlow): self.intercept_entitlements(flow) self.intercept_products(flow) def intercept_entitlements(self, flow: HTTPFlow): if BaseAddon.host_and_path_match( flow, host=OriginAddon.api_host, path=r"^/ecommerce2/entitlements/\d+$"): # Real DLC request self.patch_origin_client() log.info('Intercepted an Entitlements request from Origin ') # Get legit user entitlements try: entitlements: List = json.loads( flow.response.text)['entitlements'] except KeyError: entitlements = [] # Inject our entitlements entitlements.extend(self.injected_entitlements) # Filter out blacklisted DLCs blacklist = [ game['id'] for game in config.platforms['origin']['blacklist'] ] entitlements = [ e for e in entitlements if e['entitlementTag'] not in blacklist ] for e in entitlements: try: log.debug(f"\t{e['___name']}") except KeyError: log.debug(f"\t{e['entitlementTag']}") # Modify response flow.response.status_code = 200 flow.response.reason = 'OK' flow.response.text = json.dumps({'entitlements': entitlements}) flow.response.headers.add('X-Origin-CurrentTime', '1609452000') flow.response.headers.add('X-Origin-Signature', 'nonce') def intercept_products(self, flow: HTTPFlow): if BaseAddon.host_and_path_match( flow, host=self.api_host, path=r"^/ecommerce2/products$" ): # Just for store page, no effect in game log.info('Intercepted a Products request from Origin') tree = ElementTree.fromstring(flow.response.text) for elem in tree.iter(): if elem.tag == 'offer': log.debug(f"\t{elem.attrib['offerId']}") elif elem.tag == 'isOwned': elem.text = 'true' elif elem.tag == 'userCanPurchase': elem.text = 'false' flow.response.status_code = 200 flow.response.reason = "OK" flow.response.text = ElementTree.tostring(tree, encoding='unicode') @staticmethod def block_telemetry(flow: HTTPFlow): if config.block_telemetry and flow.request.path.startswith( '/ratt/telm'): flow.response = HTTPResponse.make(500, 'No more spying') log.debug('Blocked telemetry request from Origin') def fetch_entitlements_if_necessary(self): log.debug('Fetching Origin entitlements') # Get the etag etag_path = get_data_path('origin-entitlements.etag') etag = '' if isfile(self.entitlements_path) and isfile(etag_path): with open(etag_path, mode='r') as file: etag = file.read() # Fetch entitlements if etag does not match url = 'https://raw.githubusercontent.com/acidicoala/public-entitlements/main/origin/v1/entitlements.json' try: response = requests.get(url, headers={'If-None-Match': etag}, timeout=10) except Exception as e: log.error(f"Failed to fetch origin entitlements. {str(e)}") return if response.status_code == 304: log.debug(f'Cached Origin entitlements have not changed') return if response.status_code != 200: log.error( f'Error while fetching entitlements: {response.status_code} - {response.text}' ) return try: index = 1000000 entitlements: List[dict] = json.loads(response.text) for entitlement in entitlements: entitlement.update({ "entitlementId": index, "lastModifiedDate": "2020-01-01T00:00Z", "entitlementSource": "ORIGIN-OIG", "grantDate": "2020-01-01T00:00:00Z", "suppressedBy": [], "version": 0, "isConsumable": False, "productCatalog": "OFB", "suppressedOffers": [], "originPermissions": "0", "useCount": 0, "projectId": "123456", "status": "ACTIVE" }) index += 1 except ValueError as e: log.error(f"Failed to decode entitlements from json. {str(e)}") return # Cache entitlements with open(self.entitlements_path, 'w') as f: f.write(json.dumps(entitlements, indent=4, ensure_ascii=False)) # Cache etag with open(etag_path, 'w') as f: f.write(response.headers['etag']) log.info('Origin entitlements were successfully fetched and cached') def read_entitlements_from_cache(self): log.debug('Reading origin entitlements from cache') with open(self.entitlements_path, mode='r') as file: self.injected_entitlements: list = json.loads(file.read()) log.info( f'{len(self.injected_entitlements)} Origin entitlements were successfully read from cache' ) # Credit to anadius for the idea @synchronized_method def patch_origin_client(self): PROCESS_NAME = 'Origin.exe' DLL_NAME = 'libeay32.dll' FUNCTION_NAME = 'EVP_DigestVerifyFinal' try: origin_process = Pymem(PROCESS_NAME) except ProcessNotFound: log.warning('Origin process not found. Patching aborted') return if origin_process.process_id == self.last_origin_pid: log.debug('Origin client is already patched') return log.info('Patching Origin client') try: libeay32_module = next(m for m in origin_process.list_modules() if m.name.lower() == DLL_NAME) except StopIteration: log.error(f'{DLL_NAME} is not loaded. Patching aborted') return # The rest should complete without issues in most cases. # Get the Export Address Table symbols # noinspection PyUnresolvedReferences libeay32_dll_symbols = PE( libeay32_module.filename).DIRECTORY_ENTRY_EXPORT.symbols # Get the symbol of the EVP_DigestVerifyFinal function verify_func_symbol = next(s for s in libeay32_dll_symbols if s.name.decode('ascii') == FUNCTION_NAME) # Calculate the final address in memory verify_func_addr = libeay32_module.lpBaseOfDll + verify_func_symbol.address # Instructions to patch. We return 1 to force successful response validation. patch_instructions = bytes([ 0xB8, 0x01, 0, 0, 0, # mov eax, 0x1 0xC3, 0, 0, 0, 0 # ret ]) origin_process.write_bytes(verify_func_addr, patch_instructions, len(patch_instructions)) # Validate the written memory read_instructions = origin_process.read_bytes(verify_func_addr, len(patch_instructions)) if read_instructions != patch_instructions: log.error('Failed to patch the instruction memory') return # At this point we know that patching was successful self.last_origin_pid = origin_process.process_id log.info(f'Patching Origin was successful')
from logging import * from util.resource import get_data_path from util.singleton import Singleton log_path = get_data_path('DreamAPI.log') class Log(Logger, metaclass=Singleton): def __new__(cls, **kwargs): logger = getLogger('DreamAPI') logger.setLevel(DEBUG) formatter = Formatter( '[%(asctime)s.%(msecs)03d][%(levelname)s]\t%(message)s') formatter.datefmt = '%H:%M:%S' fileHandler = FileHandler(log_path, 'w') fileHandler.setFormatter(formatter) fileHandler.setLevel(DEBUG) consoleHandler = StreamHandler() consoleHandler.setFormatter(formatter) consoleHandler.setLevel(DEBUG) logger.addHandler(fileHandler) logger.addHandler(consoleHandler) return logger