Skip to content
Snippets Groups Projects
Commit d7e9b1b9 authored by Vincent Texier's avatar Vincent Texier
Browse files

[fix] #58 full ws2P v1 connection handshake support and example

parent 1eaddd9a
No related branches found
No related tags found
No related merge requests found
...@@ -69,7 +69,8 @@ WS2P_CONNECT_MESSAGE_SCHEMA = { ...@@ -69,7 +69,8 @@ WS2P_CONNECT_MESSAGE_SCHEMA = {
"type": "object", "type": "object",
"properties": { "properties": {
"auth": { "auth": {
"type": "string" "type": "string",
"pattern": "^CONNECT$"
}, },
"challenge": { "challenge": {
"type": "string", "type": "string",
...@@ -83,5 +84,37 @@ WS2P_CONNECT_MESSAGE_SCHEMA = { ...@@ -83,5 +84,37 @@ WS2P_CONNECT_MESSAGE_SCHEMA = {
"sig": { "sig": {
"type": "string", "type": "string",
}, },
},
"required": ["auth", "challenge", "currency", "pub", "sig"]
} }
WS2P_ACK_MESSAGE_SCHEMA = {
"type": "object",
"properties": {
"auth": {
"type": "string",
"pattern": "^ACK$"
},
"pub": {
"type": "string",
},
"sig": {
"type": "string",
}
},
"required": ["auth", "pub", "sig"]
}
WS2P_OK_MESSAGE_SCHEMA = {
"type": "object",
"properties": {
"auth": {
"type": "string",
"pattern": "^OK$"
},
"sig": {
"type": "string",
}
},
"required": ["auth", "sig"]
} }
import json
import uuid
from typing import Optional
from duniterpy.documents import Document
from duniterpy.key import VerifyingKey, SigningKey
class Connect(Document):
version = 2
auth = "CONNECT"
def __init__(self, currency: str, pubkey: str, challenge: Optional[str] = None,
signature: Optional[str] = None) -> None:
"""
Init Connect message document
:param currency: Name of currency
:param pubkey: Public key of node
:param challenge: [Optional, default=None] Big random string, typically an uuid
:param signature: [Optional, default=None] Base64 encoded signature of raw formated document
"""
super().__init__(self.version, currency, [signature])
self.pubkey = pubkey
if challenge is None:
# create challenge
self.challenge = uuid.uuid4().hex + uuid.uuid4().hex
else:
self.challenge = challenge
# add and verify signature
if signature is not None:
self.signatures.append(signature)
verifying_key = VerifyingKey(self.pubkey)
verifying_key.verify_document(self)
def raw(self):
"""
Return the document in raw format
:return:
"""
return "WS2P:CONNECT:{currency}:{pub}:{challenge}".format(currency=self.currency, pub=self.pubkey,
challenge=self.challenge)
def get_signed_json(self, signing_key: SigningKey) -> str:
"""
Return the signed document in json format
:param signing_key: Signing key instance
:return:
"""
self.sign([signing_key])
data = {
"auth": self.auth,
"pub": self.pubkey,
"challenge": self.challenge,
"sig": self.signatures[0]
}
return json.dumps(data)
def __str__(self) -> str:
return self.raw()
class Ack(Document):
version = 2
auth = "ACK"
def __init__(self, currency: str, pubkey: str, challenge: str,
signature: Optional[str] = None) -> None:
"""
Init Connect message document
:param currency: Name of currency
:param pubkey: Public key of node
:param challenge: The challenge sent in the connect message
:param signature: [Optional, default=None] Base64 encoded signature of raw formated document
"""
super().__init__(self.version, currency, [signature])
self.pubkey = pubkey
self.challenge = challenge
# add and verify signature
if signature is not None:
self.signatures.append(signature)
verifying_key = VerifyingKey(self.pubkey)
verifying_key.verify_document(self)
def raw(self):
"""
Return the document in raw format
:return:
"""
return "WS2P:ACK:{currency}:{pub}:{challenge}".format(currency=self.currency, pub=self.pubkey,
challenge=self.challenge)
def get_signed_json(self, signing_key: SigningKey) -> str:
"""
Return the signed document in json format
:param signing_key: Signing key instance
:return:
"""
self.sign([signing_key])
data = {
"auth": self.auth,
"pub": self.pubkey,
"sig": self.signatures[0]
}
return json.dumps(data)
def __str__(self) -> str:
return self.raw()
class Ok(Document):
version = 2
auth = "OK"
def __init__(self, currency: str, pubkey: str, challenge: str,
signature: Optional[str] = None) -> None:
"""
Init Connect message document
:param currency: Name of currency
:param pubkey: Public key of node
:param challenge: The challenge sent in the connect message
:param signature: [Optional, default=None] Base64 encoded signature of raw formated document
"""
super().__init__(self.version, currency, [signature])
self.pubkey = pubkey
self.challenge = challenge
# add and verify signature
if signature is not None:
self.signatures.append(signature)
verifying_key = VerifyingKey(self.pubkey)
verifying_key.verify_document(self)
def raw(self):
"""
Return the document in raw format
:return:
"""
return "WS2P:OK:{currency}:{pub}:{challenge}".format(currency=self.currency, pub=self.pubkey,
challenge=self.challenge)
def get_signed_json(self, signing_key: SigningKey) -> str:
"""
Return the signed document in json format
:param signing_key: Signing key instance
:return:
"""
self.sign([signing_key])
data = {
"auth": self.auth,
"sig": self.signatures[0]
}
return json.dumps(data)
def __str__(self) -> str:
return self.raw()
import asyncio import asyncio
from _socket import gaierror from _socket import gaierror
import aiohttp import aiohttp
import jsonschema import jsonschema
from duniterpy.api import ws2p from duniterpy.api import ws2p
from duniterpy.documents.ws2p.messages import Connect, Ack, Ok
from duniterpy.api.client import Client, parse_text from duniterpy.api.client import Client, parse_text
# CONFIG ####################################### # CONFIG #######################################
...@@ -12,7 +14,10 @@ from duniterpy.api.client import Client, parse_text ...@@ -12,7 +14,10 @@ from duniterpy.api.client import Client, parse_text
# You can either use a complete defined endpoint : [NAME_OF_THE_API] [DOMAIN] [IPv4] [IPv6] [PORT] # You can either use a complete defined endpoint : [NAME_OF_THE_API] [DOMAIN] [IPv4] [IPv6] [PORT]
# or the simple definition : [NAME_OF_THE_API] [DOMAIN] [PORT] # or the simple definition : [NAME_OF_THE_API] [DOMAIN] [PORT]
# Here we use the WS2P API (WS2P) # Here we use the WS2P API (WS2P)
from duniterpy.key import SigningKey
WS2P_ENDPOINT = "WS2P 2f731dcd 127.0.0.1 20900" WS2P_ENDPOINT = "WS2P 2f731dcd 127.0.0.1 20900"
CURRENCY = "g1-test"
################################################ ################################################
...@@ -22,6 +27,20 @@ async def main(): ...@@ -22,6 +27,20 @@ async def main():
""" """
Main code Main code
""" """
# # prompt hidden user entry
# salt = getpass.getpass("Enter your passphrase (salt): ")
#
# # prompt hidden user entry
# password = getpass.getpass("Enter your password: ")
salt = password = "toto"
# init signing_key instance
signing_key = SigningKey.from_credentials(salt, password)
connect_document = Connect(CURRENCY, signing_key.pubkey)
connect_message = connect_document.get_signed_json(signing_key)
# Create Client from endpoint string in Duniter format # Create Client from endpoint string in Duniter format
client = Client(WS2P_ENDPOINT) client = Client(WS2P_ENDPOINT)
...@@ -37,22 +56,61 @@ async def main(): ...@@ -37,22 +56,61 @@ async def main():
# It must be used in a with statement to get the ClientWebSocketResponse instance from it (__aenter__). # It must be used in a with statement to get the ClientWebSocketResponse instance from it (__aenter__).
# At the end of the with statement, aiohttp.client._WSRequestContextManager.__aexit__ is called # At the end of the with statement, aiohttp.client._WSRequestContextManager.__aexit__ is called
# and close the ClientWebSocketResponse in it. # and close the ClientWebSocketResponse in it.
connect_message = """
"""
#await ws_connection.send(connect_message)
# Mandatory to get the "for msg in ws" to work ! # Mandatory to get the "for msg in ws" to work !
async with ws_connection as ws: async with ws_connection as ws:
print("Connected successfully to web socket block path") print("Connected successfully to web socket endpoint")
print("Send message CONNECT : " + connect_message)
await ws.send_str(connect_message)
# Iterate on each message received... # Iterate on each message received...
async for msg in ws: async for msg in ws:
# if message type is text... # if message type is text...
if msg.type == aiohttp.WSMsgType.TEXT: if msg.type == aiohttp.WSMsgType.TEXT:
print("Received a message") print(msg.data)
try:
# Validate jsonschema and return a the json dict # Validate jsonschema and return a the json dict
data = parse_text(msg.data, ws2p.network.WS2P_CONNECT_MESSAGE_SCHEMA) data = parse_text(msg.data, ws2p.network.WS2P_CONNECT_MESSAGE_SCHEMA)
print(data) print("Received a CONNECT message")
remote_connect_document = Connect(CURRENCY, data["pub"], data["challenge"], data["sig"])
print("Remote CONNECT message signature is valid")
ack_message = Ack(CURRENCY, signing_key.pubkey,
remote_connect_document.challenge).get_signed_json(
signing_key)
# send ACK message
print("Send ACK message...")
await ws.send_str(ack_message)
except jsonschema.exceptions.ValidationError:
try:
# Validate jsonschema and return a the json dict
data = parse_text(msg.data, ws2p.network.WS2P_ACK_MESSAGE_SCHEMA)
print("Received a ACK message")
# create ACK document from ACK response to verify signature
Ack(CURRENCY, data["pub"], connect_document.challenge, data["sig"])
print("Remote ACK message signature is valid")
# Si ACK response ok, create OK message
ok_message = Ok(CURRENCY, signing_key.pubkey, connect_document.challenge).get_signed_json(
signing_key)
# send OK message
print("Send OK message...")
await ws.send_str(ok_message)
except jsonschema.exceptions.ValidationError:
try:
# Validate jsonschema and return a the json dict
data = parse_text(msg.data, ws2p.network.WS2P_OK_MESSAGE_SCHEMA)
print("Received a OK message")
Ok(CURRENCY, remote_connect_document.pubkey, connect_document.challenge, data["sig"])
print("Remote OK message signature is valid")
except jsonschema.exceptions.ValidationError:
pass
elif msg.type == aiohttp.WSMsgType.CLOSED: elif msg.type == aiohttp.WSMsgType.CLOSED:
# Connection is closed # Connection is closed
print("Web socket connection closed !") print("Web socket connection closed !")
...@@ -64,7 +122,7 @@ async def main(): ...@@ -64,7 +122,7 @@ async def main():
await client.close() await client.close()
except (aiohttp.WSServerHandshakeError, ValueError) as e: except (aiohttp.WSServerHandshakeError, ValueError) as e:
print("Websocket block {0} : {1}".format(type(e).__name__, str(e))) print("Websocket handshake {0} : {1}".format(type(e).__name__, str(e)))
except (aiohttp.ClientError, gaierror, TimeoutError) as e: except (aiohttp.ClientError, gaierror, TimeoutError) as e:
print("{0} : {1}".format(str(e), WS2P_ENDPOINT)) print("{0} : {1}".format(str(e), WS2P_ENDPOINT))
except jsonschema.ValidationError as e: except jsonschema.ValidationError as e:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment