Feature/client binary api update (#306)

* Split text and binary client apis

* Very initial attempt at new serialization

* Defined ser+de for Recipient and ReplySURB

* Response errors

* builds with changes

* Working WS API + moved to separate crate

* updated python examples

* Fixed parsing bug

* Updated go examples

* Updated rust examples

* formatting

* Removed unused imports

* Removed accidentally left panic call
This commit is contained in:
Jędrzej Stuczyński
2020-08-17 10:44:40 +01:00
committed by GitHub
parent eec6034684
commit 7d2d512405
31 changed files with 1900 additions and 987 deletions
@@ -0,0 +1,139 @@
import asyncio
import websockets
from pathlib import Path
import struct
# request tags
SEND_REQUEST_TAG = 0x00
REPLY_REQUEST_TAG = 0x01
SELF_ADDRESS_REQUEST_TAG = 0x02
# response tags
ERROR_RESPONSE_TAG = 0x00
RECEIVED_RESPONSE_TAG = 0x01
SELF_ADDRESS_RESPONSE_TAG = 0x02
def make_self_address_request() -> bytes:
return bytes([SELF_ADDRESS_REQUEST_TAG])
def parse_self_address_response(raw_response: bytes) -> bytes:
if len(raw_response) != 97 or raw_response[0] != SELF_ADDRESS_RESPONSE_TAG:
print('Received invalid response!')
raise
return raw_response[1:]
def make_send_request(recipient: bytes, message: bytes, with_reply_surb: bool) -> bytes:
# a big endian uint64
message_len = len(message).to_bytes(length=8, byteorder='big', signed=False)
return bytes([SEND_REQUEST_TAG]) + bytes([with_reply_surb]) + recipient + message_len + message
def make_reply_request(message: bytes, reply_surb: bytes) -> bytes:
message_len = len(message).to_bytes(length=8, byteorder='big', signed=False)
surb_len = len(reply_surb).to_bytes(length=8, byteorder='big', signed=False)
return bytes([REPLY_REQUEST_TAG]) + surb_len + reply_surb + message_len + message
# it should have structure of RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg
# where surb_len || surb is only present if 'with_reply' is true
def parse_received(raw_response: bytes) -> (bytes, bytes):
if raw_response[0] != RECEIVED_RESPONSE_TAG:
print('Received invalid response!')
raise
if raw_response[1] == 1:
has_surb = True
elif raw_response[1] == 0:
has_surb = False
else:
print("malformed received response!")
raise
data = raw_response[2:]
if has_surb:
(surb_len,), other = struct.unpack(">Q", data[:8]), data[8:]
surb = other[:surb_len]
(msg_len,) = struct.unpack(">Q", other[surb_len:surb_len + 8])
if len(other[surb_len + 8:]) != msg_len:
print("invalid msg len")
raise
msg = other[surb_len + 8:]
return msg, surb
else:
(msg_len,), other = struct.unpack(">Q", data[:8]), data[8:]
if len(other) != msg_len:
print("invalid msg len")
raise
msg = other[:msg_len]
return msg, None
async def send_file_with_reply():
uri = "ws://localhost:1977"
async with websockets.connect(uri) as websocket:
self_address_req = make_self_address_request()
await websocket.send(self_address_req)
self_address = parse_self_address_response(await websocket.recv())
file_data = Path('dummy_file').read_bytes()
send_request = make_send_request(self_address, file_data, True)
print("sending content of 'dummy_file' over the mix network...")
await websocket.send(send_request)
print("waiting to receive the 'dummy_file' from the mix network...")
received_response = await websocket.recv()
received_file, surb = parse_received(received_response)
with open("received_file_withreply", "wb") as output_file:
print("writing the file back to the disk!")
output_file.write(received_file)
reply_message = b"hello from reply SURB! - thanks for sending me the file!"
reply_request = make_reply_request(reply_message, surb)
print("sending '{}' (using reply SURB!) over the mix network...".format(reply_message))
await websocket.send(reply_request)
print("waiting to receive a message from the mix network...")
received_response = await websocket.recv()
received_msg, surb = parse_received(received_response)
assert surb is None # no surbs in replies!
print("received '{}' from the mix network".format(received_msg))
async def send_file_without_reply():
uri = "ws://localhost:1977"
async with websockets.connect(uri) as websocket:
self_address_req = make_self_address_request()
await websocket.send(self_address_req)
self_address = parse_self_address_response(await websocket.recv())
file_data = Path('dummy_file').read_bytes()
send_request = make_send_request(self_address, file_data, False)
print("sending content of 'dummy_file' over the mix network...")
await websocket.send(send_request)
print("waiting to receive the 'dummy_file' from the mix network...")
received_response = await websocket.recv()
received_file, surb = parse_received(received_response)
assert surb is None # we didn't attach a surb so we expect a None here!
with open("received_file_noreply", "wb") as output_file:
print("writing the file back to the disk!")
output_file.write(received_file)
# asyncio.get_event_loop().run_until_complete(send_file_without_reply())
asyncio.get_event_loop().run_until_complete(send_file_with_reply())
@@ -1,111 +0,0 @@
import asyncio
import base58
import json
import websockets
self_address_request = json.dumps({
"type": "selfAddress"
})
WITHOUT_REPLY: int = 0
WITH_REPLY: int = 1
REPLY: int = 2
# this is really flaky so ideally will be replaced by some proper serialization
# 16 - length of encryption key
# 32 - length of first hop
# 348 - current sphinx header size
# 192 - size of single payload key
# 4 - number of mix hops (gateway + 3 nodes)
REPLY_SURB_LEN: int = 16 + 32 + 348 + 4 * 192
async def send_file_with_reply():
uri = "ws://localhost:1977"
async with websockets.connect(uri) as websocket:
await websocket.send(self_address_request)
self_address = json.loads(await websocket.recv())
print("our address is: {}".format(self_address["address"]))
# we receive our address in string format of OUR_ID_PUB_KEY . OUR_ENC_PUB_KEY @ OUR_GATE_ID_PUB_KEY
# all keys are 32 bytes and we need to encode them as binary without the '.' or '@' signs
split_address = self_address["address"].split("@")
client_half = split_address[0]
gateway_half = split_address[1]
# to DH: I think at this point this should be replaced with some sort of protobuf / flatbuffers / Cap'n Proto, etc
split_client_address = client_half.split(".")
bin_payload = bytearray([WITH_REPLY])
bin_payload += base58.b58decode(split_client_address[0])
bin_payload += base58.b58decode(split_client_address[1])
bin_payload += base58.b58decode(gateway_half)
with open("dummy_file", "rb") as input_file:
read_data = input_file.read()
bin_payload += read_data
print("sending content of 'dummy_file' over the mix network...")
await websocket.send(bin_payload)
print("waiting to receive the 'dummy_file' from the mix network...")
received_data = await websocket.recv()
assert received_data[0] == WITH_REPLY
reply_surb = received_data[1:1 +REPLY_SURB_LEN]
output_file_data = received_data[1 + REPLY_SURB_LEN:]
with open("received_file_withreply", "wb") as output_file:
print("writing the file back to the disk!")
output_file.write(output_file_data)
reply_message = b"hello from reply SURB! - thanks for sending me the file!"
binary_reply = bytearray([REPLY])
binary_reply += reply_surb
binary_reply += reply_message
print("sending '{}' (using reply SURB!) over the mix network...".format(reply_message))
await websocket.send(binary_reply)
print("waiting to receive a message from the mix network...")
received_reply = await websocket.recv()
assert received_reply[0] == WITHOUT_REPLY
print("received '{}' from the mix network".format(received_reply[1:]))
async def send_file_without_reply():
uri = "ws://localhost:1977"
async with websockets.connect(uri) as websocket:
await websocket.send(self_address_request)
self_address = json.loads(await websocket.recv())
print("our address is: {}".format(self_address["address"]))
# we receive our address in string format of OUR_ID_PUB_KEY . OUR_ENC_PUB_KEY @ OUR_GATE_ID_PUB_KEY
# all keys are 32 bytes and we need to encode them as binary without the '.' or '@' signs
split_address = self_address["address"].split("@")
client_half = split_address[0]
gateway_half = split_address[1]
# to DH: I think at this point this should be replaced with some sort of protobuf / flatbuffers / Cap'n Proto, etc
split_client_address = client_half.split(".")
bin_payload = bytearray([WITHOUT_REPLY])
bin_payload += base58.b58decode(split_client_address[0])
bin_payload += base58.b58decode(split_client_address[1])
bin_payload += base58.b58decode(gateway_half)
with open("dummy_file", "rb") as input_file:
read_data = input_file.read()
bin_payload += read_data
print("sending content of 'dummy_file' over the mix network...")
await websocket.send(bin_payload)
print("waiting to receive the 'dummy_file' from the mix network...")
received_data = await websocket.recv()
assert received_data[0] == WITHOUT_REPLY
with open("received_file_noreply", "wb") as output_file:
print("writing the file back to the disk!")
output_file.write(received_data[1:])
# asyncio.get_event_loop().run_until_complete(send_file_without_reply())
asyncio.get_event_loop().run_until_complete(send_file_with_reply())
@@ -55,13 +55,13 @@ async def send_text_with_reply():
print("received '{}' from the mix network".format(received_message))
# use the received surb to send an anonymous reply!
reply_surb = received_message["replySurb"]
reply_surb = received_message["replySURB"]
reply_message = "hello from reply SURB!"
reply = json.dumps({
"type": "reply",
"message": reply_message,
"replySurb": reply_surb
"replySURB": reply_surb
})
print("sending '{}' (using reply SURB!) over the mix network...".format(reply_message))