Commit 794c31d3 authored by Vincent Texier's avatar Vincent Texier Committed by Vincent Texier

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

parent dd050aa0
......@@ -69,7 +69,8 @@ WS2P_CONNECT_MESSAGE_SCHEMA = {
"type": "object",
"properties": {
"auth": {
"type": "string"
"type": "string",
"pattern": "^CONNECT$"
},
"challenge": {
"type": "string",
......@@ -83,5 +84,37 @@ WS2P_CONNECT_MESSAGE_SCHEMA = {
"sig": {
"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
from _socket import gaierror
import aiohttp
import jsonschema
from duniterpy.api import ws2p
from duniterpy.documents.ws2p.messages import Connect, Ack, Ok
from duniterpy.api.client import Client, parse_text
# CONFIG #######################################
......@@ -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]
# or the simple definition : [NAME_OF_THE_API] [DOMAIN] [PORT]
# Here we use the WS2P API (WS2P)
from duniterpy.key import SigningKey
WS2P_ENDPOINT = "WS2P 2f731dcd 127.0.0.1 20900"
CURRENCY = "g1-test"
################################################
......@@ -22,6 +27,20 @@ async def main():
"""
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
client = Client(WS2P_ENDPOINT)
......@@ -37,22 +56,61 @@ async def main():
# 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
# and close the ClientWebSocketResponse in it.
connect_message = """
"""
#await ws_connection.send(connect_message)
# Mandatory to get the "for msg in ws" to work !
async with ws_connection as ws:
print("Connected successfully to web socket block path")
print("Connected successfully to web socket endpoint")
print("Send CONNECT message")
await ws.send_str(connect_message)
# Iterate on each message received...
async for msg in ws:
# if message type is text...
if msg.type == aiohttp.WSMsgType.TEXT:
print("Received a message")
# Validate jsonschema and return a the json dict
data = parse_text(msg.data, ws2p.network.WS2P_CONNECT_MESSAGE_SCHEMA)
print(data)
# print(msg.data)
try:
# Validate json string with jsonschema and return a dict
data = parse_text(msg.data, ws2p.network.WS2P_CONNECT_MESSAGE_SCHEMA)
print("Received a CONNECT message")
remote_connect_document = Connect(CURRENCY, data["pub"], data["challenge"], data["sig"])
print("Received 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 json string with jsonschema and return a 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("Received 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 json string with jsonschema and return a 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("Received OK message signature is valid")
except jsonschema.exceptions.ValidationError:
pass
elif msg.type == aiohttp.WSMsgType.CLOSED:
# Connection is closed
print("Web socket connection closed !")
......@@ -60,11 +118,11 @@ async def main():
# Connection error
print("Web socket connection error !")
# Close session
await client.close()
# Close session
await client.close()
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:
print("{0} : {1}".format(str(e), WS2P_ENDPOINT))
except jsonschema.ValidationError as e:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment