def exchange(self, uri: str, tree: Node, text_encoding: str="shift-jis", packet_encoding: str="binary") -> Node: headers = {} if self.__verbose: print('Outgoing request:') print(tree) # Handle encoding if packet_encoding == "xml": _packet_encoding = EAmuseProtocol.XML elif packet_encoding == "binary": _packet_encoding = EAmuseProtocol.BINARY else: raise Exception(f"Unknown packet encoding {packet_encoding}") # Handle encryption if self.__encryption: encryption = f'1-{hex_string(8)}-{hex_string(4)}' headers['X-Eamuse-Info'] = encryption else: encryption = None # Handle compression if self.__compression: compression = 'lz77' else: compression = None headers['X-Compress'] = compression # Convert it proto = EAmuseProtocol() req = proto.encode( compression, encryption, tree, text_encoding=text_encoding, packet_encoding=_packet_encoding, ) # Send the request, get the response r = requests.post( f'http://{self.__address}:{self.__port}{"/" if uri[0] != "/" else ""}{uri}', headers=headers, data=req, ) # Get the compression and encryption encryption = headers.get('X-Eamuse-Info') compression = headers.get('X-Compress') # Decode it packet = proto.decode( compression, encryption, r.content, ) if self.__verbose: print('Incoming response:') print(packet) return packet
def assertLoopback(self, root: Node) -> Optional[int]: proto = EAmuseProtocol() for encoding in [EAmuseProtocol.BINARY, EAmuseProtocol.XML]: if encoding == EAmuseProtocol.BINARY: loop_name = "binary" elif encoding == EAmuseProtocol.XML: loop_name = "xml" else: raise Exception("Logic error!") binary = proto.encode(None, None, root, text_encoding=EAmuseProtocol.SHIFT_JIS, packet_encoding=encoding) newroot = proto.decode(None, None, binary) self.assertEqual( newroot, root, "Round trip with {} and no encryption/compression doesn't match!" .format(loop_name)) binary = proto.encode(None, '1-abcdef-0123', root, text_encoding=EAmuseProtocol.SHIFT_JIS, packet_encoding=encoding) newroot = proto.decode(None, '1-abcdef-0123', binary) self.assertEqual( newroot, root, "Round trip with {}, encryption and no compression doesn't match!" .format(loop_name)) binary = proto.encode('none', None, root, text_encoding=EAmuseProtocol.SHIFT_JIS, packet_encoding=encoding) newroot = proto.decode('none', None, binary) self.assertEqual( newroot, root, "Round trip with {}, encryption and no compression doesn't match!" .format(loop_name)) binary = proto.encode('lz77', None, root, text_encoding=EAmuseProtocol.SHIFT_JIS, packet_encoding=encoding) newroot = proto.decode('lz77', None, binary) self.assertEqual( newroot, root, "Round trip with {}, no encryption and lz77 compression doesn't match!" .format(loop_name))
def main() -> None: parser = argparse.ArgumentParser(description="A utility to replay a packet from a log or binary dump.") parser.add_argument("-i", "--infile", help="File containing an XML or binary node structure. Use - for stdin.", type=str, default=None, required=True) parser.add_argument("-e", "--encoding", help="Encoding for the packet, defaults to UTF-8.", type=str, default='utf-8') parser.add_argument("-p", "--port", help="Port to talk to. Defaults to 80", type=int, default=80) parser.add_argument("-a", "--address", help="Address to talk to. Defaults to 127.0.0.1", type=str, default="127.0.0.1") parser.add_argument("-u", "--path", help="URI that we should post to. Defaults to '/'", type=str, default="/") args = parser.parse_args() if args.infile == '-': # Load from stdin packet = sys.stdin.buffer.read() else: with open(args.infile, mode="rb") as myfile: packet = myfile.read() myfile.close # Add an XML special node to force encoding (will be overwritten if there # is one in the packet). packet = b''.join([ f'<?xml encoding="{args.encoding}"?>'.encode(args.encoding), packet, ]) # Attempt to decode it proto = EAmuseProtocol() tree = proto.decode( None, None, packet, ) if tree is None: # Can't decode, exit raise Exception("Unable to decode packet!") model = tree.attribute('model') module = tree.children[0].name method = tree.children[0].attribute('method') server = Protocol(args.address, args.port, False, False, False) server.exchange( f'{args.path}?model={model}&module={module}&method={method}', tree, )
def generate_code(infile: str, outfile: str, encoding: str) -> None: if infile == '-': # Load from stdin packet = sys.stdin.buffer.read() else: with open(infile, mode="rb") as infp: packet = infp.read() infp.close # Add an XML special node to force encoding (will be overwritten if there # is one in the packet). packet = b''.join([ f'<?xml encoding="{encoding}"?>'.encode(encoding), packet, ]) # Attempt to decode it proto = EAmuseProtocol() req = proto.decode( None, None, packet, ) if req is None: # Can't decode, exit raise Exception("Unable to decode packet!") # Walk through, outputting each node and attaching it to its parent code = '\n'.join(generate_lines(req, {})) if outfile == '-': print(code) else: with open(outfile, mode="a") as outfp: outfp.write(code) outfp.close
def mainloop(address: Optional[str] = None, port: int = 80, verbose: bool = False) -> None: """ Main loop of BEMANIShark. Starts an instance of Sniffer and EAmuseProtocol and does a lazy job of banging them together with the above HTTP.parse. Will loop trying to decode packets forever. Arguments: address - A string representing an IP of interest port - An integer representing a port of interest """ sniffer = Sniffer(address=address, port=port) parser = EAmuseProtocol() while True: packets = sniffer.recv_stream() inbound = HTTP.parse(packets['inbound'], request=True) outbound = HTTP.parse(packets['outbound'], response=True) if inbound is not None: if inbound['data'] is None: in_req = None else: try: in_req = parser.decode( inbound['headers'].get('x-compress'), inbound['headers'].get('x-eamuse-info'), inbound['data']) except EAmuseException: in_req = None print("Inbound request (from {}:{} to {}:{}):".format( packets['source_address'], packets['source_port'], packets['destination_address'], packets['destination_port'], )) if verbose: print("HTTP {} request for URI {}".format( inbound['method'], inbound['uri'])) print("Compression is {}".format(inbound['headers'].get( 'x-compress', 'none'))) print("Encryption key is {}".format(inbound['headers'].get( 'x-eamuse-info', 'none'))) if in_req is None: print("Inbound request was not parseable") else: print(in_req) if outbound is not None: if outbound['data'] is None: out_req = None else: try: out_req = parser.decode( outbound['headers'].get('x-compress'), outbound['headers'].get('x-eamuse-info'), outbound['data']) except EAmuseException: out_req = None print("Outbound response (from {}:{} to {}:{}):".format( packets['destination_address'], packets['destination_port'], packets['source_address'], packets['source_port'], )) if verbose: print("Compression is {}".format(outbound['headers'].get( 'x-compress', 'none'))) print("Encryption key is {}".format(outbound['headers'].get( 'x-eamuse-info', 'none'))) if out_req is None: print("Outbound response was not parseable") else: print(out_req)
def receive_request(path: str) -> Response: # First, parse the packet itself client_proto = EAmuseProtocol() server_proto = EAmuseProtocol() remote_address = request.headers.get('X-Remote-Address', None) request_compression = request.headers.get('X-Compress', None) request_encryption = request.headers.get('X-Eamuse-Info', None) actual_path = f'/{path}' if request.query_string is not None and len(request.query_string) > 0: actual_path = actual_path + f'?{request.query_string.decode("ascii")}' if config['verbose']: print(f"HTTP request for URI {actual_path}") print(f"Compression is {request_compression}") print(f"Encryption key is {request_encryption}") req = client_proto.decode( request_compression, request_encryption, request.data, ) if req is None: # Nothing to do here return Response("Unrecognized packet!", 500) if config['verbose']: print("Original request to server:") print(req) # Grab PCBID for directing to mulitple servers pcbid = req.attribute('srcid') if pcbid in config['remote']: remote_host = config['remote'][pcbid]['host'] remote_port = config['remote'][pcbid]['port'] elif '*' in config['remote']: remote_host = config['remote']['*']['host'] remote_port = config['remote']['*']['port'] else: return Response(f"No route for PCBID {pcbid}", 500) modified_request = modify_request(config, req) if modified_request is None: # Return the original binary data instead of re-encoding it # to the exact same thing. req_binary = request.data else: if config['verbose']: print("Modified request to server:") print(modified_request) # Re-encode the modified packet req_binary = server_proto.encode( request_compression, request_encryption, modified_request, client_proto.last_text_encoding, client_proto.last_packet_encoding, ) # Set up custom headers for remote request headers = {} if request_compression is not None: headers['X-Compress'] = request_compression if request_encryption is not None: headers['X-Eamuse-Info'] = request_encryption # For lobby functionality, make sure the request receives # the original IP address headers['X-Remote-Address'] = remote_address or request.remote_addr # Make request to foreign service, using the same parameters r = requests.post( f'http://{remote_host}:{remote_port}{actual_path}', headers=headers, data=req_binary, timeout=config['timeout'], ) if r.status_code != 200: # Failed on remote side return Response("Failed to get response!", 500) # Decode response, for modification if necessary response_compression = r.headers.get('X-Compress', None) response_encryption = r.headers.get('X-Eamuse-Info', None) resp = server_proto.decode( response_compression, response_encryption, r.content, ) if resp is None: # Nothing to do here return Response("Unrecognized packet!", 500) if config['verbose']: print("Original response from server:") print(resp) modified_response = modify_response(config, resp) if modified_response is None: # Return the original response data instead of re-encoding it # to the exact same thing. resp_binary = r.content else: if config['verbose']: print("Modified response from server:") print(modified_response) # Re-encode the modified packet resp_binary = client_proto.encode( response_compression, response_encryption, modified_response, ) # Some old clients are case sensitive, so be careful to capitalize # these responses here. flask_resp = Response(resp_binary) if response_compression is not None: flask_resp.headers['X-Compress'] = response_compression if response_encryption is not None: flask_resp.headers['X-Eamuse-Info'] = response_encryption return flask_resp
def receive_request(path: str) -> Response: proto = EAmuseProtocol() remote_address = request.headers.get('x-remote-address', None) compression = request.headers.get('x-compress', None) encryption = request.headers.get('x-eamuse-info', None) req = proto.decode( compression, encryption, request.data, ) if req is None: # Nothing to do here return Response("Unrecognized packet!", 500) if req.name in {'soapenv:Envelope', 'soap:Envelope', 'methodCall'}: # We get lots of spam from random bots trying to SOAP # us up, so ignore this shit. return Response("Unrecognized packet!", 500) # Create and format config global config requestconfig = copy.copy(config) requestconfig['client'] = { 'address': remote_address or request.remote_addr, } dataprovider = Data(requestconfig) try: dispatch = Dispatch(requestconfig, dataprovider, True) resp = dispatch.handle(req) if resp is None: # Nothing to do here dataprovider.local.network.put_event( 'unhandled_packet', { 'request': str(req), }, ) return Response("No response generated", 404) compression = None data = proto.encode( compression, encryption, resp, ) response = make_response(data) # Some old clients are case-sensitive, even though http spec says these # shouldn't matter, so capitalize correctly. if compression: response.headers['X-Compress'] = compression else: response.headers['X-Compress'] = 'none' if encryption: response.headers['X-Eamuse-Info'] = encryption return response except UnrecognizedPCBIDException as e: dataprovider.local.network.put_event( 'unauthorized_pcbid', { 'pcbid': e.pcbid, 'model': e.model, 'ip': e.ip, }, ) return Response("Unauthorized client", 403) except Exception: stack = traceback.format_exc() print(stack) dataprovider.local.network.put_event( 'exception', { 'service': 'xrpc', 'request': str(req), 'traceback': stack, }, ) return Response("Crash when handling packet!", 500) finally: dataprovider.close()