Compare commits

...

11 Commits
main ... dev

13 changed files with 1143 additions and 551 deletions

View File

@ -16,3 +16,24 @@ repos:
pass_filenames: false pass_filenames: false
language: system language: system
stages: [push] stages: [push]
- id: black
name: black
description: "Black: The uncompromising Python code formatter"
entry: poetry run black
args: ["--diff"]
language: system
require_serial: true
types_or: [python, pyi]
- id: pylint
name: pylint
entry: poetry run pylint
language: system
types: [python]
require_serial: true
- id: isort
name: isort
args: ["--check", "--diff"]
entry: poetry run isort
language: system
require_serial: true
types_or: [python, pyi]

View File

@ -1,93 +1,108 @@
"""Example SSB Client"""
from asyncio import ensure_future, gather, get_event_loop
import base64
import hashlib
import logging import logging
import struct import struct
import time import time
from asyncio import get_event_loop, gather, ensure_future
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
from nacl.signing import SigningKey
from secret_handshake.network import SHSClient from secret_handshake.network import SHSClient
from ssb.muxrpc import MuxRPCAPI, MuxRPCAPIException
from ssb.muxrpc import MuxRPCAPI, MuxRPCAPIException, MuxRPCRequest
from ssb.packet_stream import PacketStream, PSMessageType from ssb.packet_stream import PacketStream, PSMessageType
from ssb.util import load_ssb_secret from ssb.util import load_ssb_secret
import hashlib
import base64
api = MuxRPCAPI() api = MuxRPCAPI()
@api.define('createHistoryStream') @api.define("createHistoryStream")
def create_history_stream(connection, msg): def create_history_stream(connection: PacketStream, msg: MuxRPCRequest) -> None: # pylint: disable=unused-argument
print('create_history_stream', msg) """Handle the createHistoryStream RPC call"""
print("create_history_stream", msg)
# msg = PSMessage(PSMessageType.JSON, True, stream=True, end_err=True, req=-req) # msg = PSMessage(PSMessageType.JSON, True, stream=True, end_err=True, req=-req)
# connection.write(msg) # connection.write(msg)
@api.define('blobs.createWants') @api.define("blobs.createWants")
def create_wants(connection, msg): def create_wants(connection: PacketStream, msg: MuxRPCRequest) -> None: # pylint: disable=unused-argument
print('create_wants', msg) """Handle the createWants RPC call"""
print("create_wants", msg)
async def test_client(): async def test_client() -> None:
async for msg in api.call('createHistoryStream', [{ """The actual client implementation"""
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
'seq': 1, async for msg in api.call(
'live': False, "createHistoryStream",
'keys': False [{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}],
}], 'source'): "source",
print('> RESPONSE:', msg) ):
print("> RESPONSE:", msg)
try: try:
print('> RESPONSE:', await api.call('whoami', [], 'sync')) print("> RESPONSE:", await api.call("whoami", [], "sync"))
except MuxRPCAPIException as e: except MuxRPCAPIException as e:
print(e) print(e)
handler = api.call('gossip.ping', [], 'duplex') handler = api.call("gossip.ping", [], "duplex")
handler.send(struct.pack('l', int(time.time() * 1000)), msg_type=PSMessageType.BUFFER) handler.send(struct.pack("l", int(time.time() * 1000)), msg_type=PSMessageType.BUFFER)
async for msg in handler: async for msg in handler:
print('> RESPONSE:', msg) print("> RESPONSE:", msg)
handler.send(True, end=True) handler.send(True, end=True)
break break
img_data = b'' img_data = b""
async for msg in api.call('blobs.get', ['&kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM=.sha256'], 'source'): async for msg in api.call("blobs.get", ["&kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM=.sha256"], "source"):
if msg.type.name == 'BUFFER': assert msg
if msg.type.name == "BUFFER":
img_data += msg.data img_data += msg.data
if msg.type.name == 'JSON' and msg.data == b'true':
assert (base64.b64encode(hashlib.sha256(img_data).digest()) == if msg.type.name == "JSON" and msg.data == b"true":
b'kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM=') assert (
with open('./ub1k.jpg', 'wb') as f: base64.b64encode(hashlib.sha256(img_data).digest()) == b"kqZ52sDcJSHOx7m4Ww80kK1KIZ65gpGnqwZlfaIVWWM="
)
with open("./ub1k.jpg", "wb") as f:
f.write(img_data) f.write(img_data)
async def main(): async def main(keypair: SigningKey) -> None:
client = SHSClient('127.0.0.1', 8008, keypair, bytes(keypair.verify_key)) """The main function to run"""
client = SHSClient("127.0.0.1", 8008, keypair, bytes(keypair.verify_key))
packet_stream = PacketStream(client) packet_stream = PacketStream(client)
await client.open() await client.open()
api.add_connection(packet_stream) api.add_connection(packet_stream)
await gather(ensure_future(api), test_client()) await gather(ensure_future(api), test_client())
if __name__ == '__main__': if __name__ == "__main__":
# create console handler and set level to debug # create console handler and set level to debug
ch = logging.StreamHandler() ch = logging.StreamHandler()
ch.setLevel(logging.INFO) ch.setLevel(logging.INFO)
# create formatter # create formatter
formatter = ColoredFormatter('%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - ' formatter = ColoredFormatter(
'%(cyan)s%(message)s%(reset)s') "%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - %(cyan)s%(message)s%(reset)s"
)
# add formatter to ch # add formatter to ch
ch.setFormatter(formatter) ch.setFormatter(formatter)
# add ch to logger # add ch to logger
logger = logging.getLogger('packet_stream') logger = logging.getLogger("packet_stream")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
logger.addHandler(ch) logger.addHandler(ch)
keypair = load_ssb_secret()['keypair'] ssb_keypair = load_ssb_secret()["keypair"]
loop = get_event_loop() loop = get_event_loop()
loop.run_until_complete(main()) loop.run_until_complete(main(ssb_keypair))
loop.close() loop.close()

View File

@ -1,45 +1,53 @@
"""Test SSB server"""
from asyncio import get_event_loop
import logging import logging
from asyncio import gather, get_event_loop, ensure_future
from colorlog import ColoredFormatter from colorlog import ColoredFormatter
from secret_handshake import SHSServer from secret_handshake import SHSServer
from ssb.packet_stream import PacketStream from secret_handshake.network import SHSDuplexStream
from ssb.muxrpc import MuxRPCAPI from ssb.muxrpc import MuxRPCAPI
from ssb.packet_stream import PacketStream
from ssb.util import load_ssb_secret from ssb.util import load_ssb_secret
api = MuxRPCAPI() api = MuxRPCAPI()
async def on_connect(conn): async def on_connect(conn: SHSDuplexStream) -> None:
"""Incoming connection handler"""
packet_stream = PacketStream(conn) packet_stream = PacketStream(conn)
api.add_connection(packet_stream) api.add_connection(packet_stream)
print('connect', conn) print("connect", conn)
async for msg in packet_stream: async for msg in packet_stream:
print(msg) print(msg)
async def main(): async def main() -> None:
server = SHSServer('127.0.0.1', 8008, load_ssb_secret()['keypair']) """The main function to run"""
server = SHSServer("127.0.0.1", 8008, load_ssb_secret()["keypair"])
server.on_connect(on_connect) server.on_connect(on_connect)
await server.listen() await server.listen()
if __name__ == '__main__': if __name__ == "__main__":
# create console handler and set level to debug # create console handler and set level to debug
ch = logging.StreamHandler() ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG) ch.setLevel(logging.DEBUG)
# create formatter # create formatter
formatter = ColoredFormatter('%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - ' formatter = ColoredFormatter(
'%(cyan)s%(message)s%(reset)s') "%(log_color)s%(levelname)s%(reset)s:%(bold_white)s%(name)s%(reset)s - %(cyan)s%(message)s%(reset)s"
)
# add formatter to ch # add formatter to ch
ch.setFormatter(formatter) ch.setFormatter(formatter)
# add ch to logger # add ch to logger
logger = logging.getLogger('packet_stream') logger = logging.getLogger("packet_stream")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logger.addHandler(ch) logger.addHandler(ch)

340
poetry.lock generated
View File

@ -26,16 +26,19 @@ files = [
test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] test = ["coverage", "mypy", "pexpect", "ruff", "wheel"]
[[package]] [[package]]
name = "async-generator" name = "astroid"
version = "1.8" version = "3.0.1"
description = "Async generators for Python 3.5" description = "An abstract syntax tree for Python with inference support."
optional = false optional = false
python-versions = "*" python-versions = ">=3.8.0"
files = [ files = [
{file = "async_generator-1.8-py3-none-any.whl", hash = "sha256:d9253336202cb9df50ba617893fe794c61394a7eb4b9054f285c860f395ac6ff"}, {file = "astroid-3.0.1-py3-none-any.whl", hash = "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca"},
{file = "async_generator-1.8.zip", hash = "sha256:928b644cfc92be498f2d6c431e0082ae79ea736fbdf1ce4247881071dd525348"}, {file = "astroid-3.0.1.tar.gz", hash = "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"},
] ]
[package.dependencies]
typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
[[package]] [[package]]
name = "babel" name = "babel"
version = "2.13.1" version = "2.13.1"
@ -53,6 +56,48 @@ setuptools = {version = "*", markers = "python_version >= \"3.12\""}
[package.extras] [package.extras]
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
[[package]]
name = "black"
version = "23.10.1"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"},
{file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"},
{file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"},
{file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"},
{file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"},
{file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"},
{file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"},
{file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"},
{file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"},
{file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"},
{file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"},
{file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"},
{file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"},
{file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"},
{file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"},
{file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"},
{file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"},
{file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2023.7.22" version = "2023.7.22"
@ -244,6 +289,20 @@ toml = "*"
[package.extras] [package.extras]
test = ["mock"] test = ["mock"]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
@ -374,6 +433,20 @@ files = [
{file = "decli-0.6.1.tar.gz", hash = "sha256:ed88ccb947701e8e5509b7945fda56e150e2ac74a69f25d47ac85ef30ab0c0f0"}, {file = "decli-0.6.1.tar.gz", hash = "sha256:ed88ccb947701e8e5509b7945fda56e150e2ac74a69f25d47ac85ef30ab0c0f0"},
] ]
[[package]]
name = "dill"
version = "0.3.7"
description = "serialize all of Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"},
{file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"},
]
[package.extras]
graph = ["objgraph (>=1.7.2)"]
[[package]] [[package]]
name = "docutils" name = "docutils"
version = "0.17.1" version = "0.17.1"
@ -453,20 +526,20 @@ files = [
[[package]] [[package]]
name = "isort" name = "isort"
version = "4.3.21" version = "5.12.0"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=3.8.0"
files = [ files = [
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
] ]
[package.extras] [package.extras]
pipfile = ["pipreqs", "requirementslib"] colors = ["colorama (>=0.4.3)"]
pyproject = ["toml"] pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
requirements = ["pip-api", "pipreqs"] plugins = ["setuptools"]
xdg-home = ["appdirs (>=1.4.0)"] requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
@ -554,6 +627,74 @@ files = [
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
] ]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy"
version = "1.6.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"},
{file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"},
{file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"},
{file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"},
{file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"},
{file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"},
{file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"},
{file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"},
{file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"},
{file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"},
{file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"},
{file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"},
{file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"},
{file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"},
{file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"},
{file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"},
{file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"},
{file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"},
{file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"},
{file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"},
{file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"},
{file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"},
{file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"},
{file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"},
{file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"},
{file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"},
{file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=4.1.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.2" version = "23.2"
@ -565,6 +706,17 @@ files = [
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
] ]
[[package]]
name = "pathspec"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.7"
files = [
{file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
{file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
]
[[package]] [[package]]
name = "pep257" name = "pep257"
version = "0.7.0" version = "0.7.0"
@ -576,6 +728,21 @@ files = [
{file = "pep257-0.7.0.tar.gz", hash = "sha256:f3d67547f5617a9cfeb4b8097ed94a954888315defaf6e9b518ff1719363bf03"}, {file = "pep257-0.7.0.tar.gz", hash = "sha256:f3d67547f5617a9cfeb4b8097ed94a954888315defaf6e9b518ff1719363bf03"},
] ]
[[package]]
name = "platformdirs"
version = "3.11.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.7"
files = [
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
]
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.3.0" version = "1.3.0"
@ -630,49 +797,61 @@ files = [
[package.extras] [package.extras]
plugins = ["importlib-metadata"] plugins = ["importlib-metadata"]
[[package]]
name = "pylint"
version = "3.0.2"
description = "python code static checker"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "pylint-3.0.2-py3-none-any.whl", hash = "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda"},
{file = "pylint-3.0.2.tar.gz", hash = "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496"},
]
[package.dependencies]
astroid = ">=3.0.1,<=3.1.0-dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
{version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
]
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.8"
platformdirs = ">=2.2.0"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
tomlkit = ">=0.10.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[package.extras]
spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]] [[package]]
name = "pynacl" name = "pynacl"
version = "1.1.2" version = "1.5.0"
description = "Python binding to the Networking and Cryptography (NaCl) library" description = "Python binding to the Networking and Cryptography (NaCl) library"
optional = false optional = false
python-versions = "*" python-versions = ">=3.6"
files = [ files = [
{file = "PyNaCl-1.1.2-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:9558ef5c1ae45322c054d1d1151016e0463b4da8b5c746a675e99c5c7d8f4faa"}, {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"},
{file = "PyNaCl-1.1.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:301c966c1e17950e50d174ab4b2e7ef3e98ff51ad7a591152a19fe2139281eed"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"},
{file = "PyNaCl-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d21d733a63637ddf41d0cab50135ec9f5224dd22fd10ebf5c5f5f946b833f84"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"},
{file = "PyNaCl-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:c93d151efcdd7d214b1b11d781c9f1b125f0208cd06d9762bddabdfeac1cedfc"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"},
{file = "PyNaCl-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:1b4938a557b32e5c6b27fac79a94cf1abb70753b5462a0b577bd2a77e09dacd0"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"},
{file = "PyNaCl-1.1.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:99f91eb80b85fe16f70d362cfeae8eeeb108cd09a85f039fdab02164762f764b"}, {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"},
{file = "PyNaCl-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57314a7bad4bd39501dc622942f9921923673e52e126b0fc4f0214b5d25d619a"}, {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"},
{file = "PyNaCl-1.1.2-cp33-cp33m-macosx_10_6_intel.whl", hash = "sha256:506bc2591968a1a7b6577075bc29a591d8fff5bdfec03b0dd926f34b75b670e5"}, {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"},
{file = "PyNaCl-1.1.2-cp33-cp33m-manylinux1_i686.whl", hash = "sha256:4c15d7cea1a313fff3f68222e682ee1f855e43c0865081cad7385066a6b57d75"}, {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"},
{file = "PyNaCl-1.1.2-cp33-cp33m-manylinux1_x86_64.whl", hash = "sha256:c4ea0e3b9f3317ada56e12c7b37f6d0316900ae8b54a20d7b100d4e14350ac87"}, {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"},
{file = "PyNaCl-1.1.2-cp33-cp33m-win32.whl", hash = "sha256:53d83faf274813a5778bba1cd4cb96b79f39e44a63b1c4a4dada01a2b0eeafe8"},
{file = "PyNaCl-1.1.2-cp33-cp33m-win_amd64.whl", hash = "sha256:5172395dea8203ae124fd282fef3d242aa75366d66aebc0f5aab0c4753eed97b"},
{file = "PyNaCl-1.1.2-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:877879903cddb5da317fe86d923f65eb28c62fd7feb79cd3402d166e401f9423"},
{file = "PyNaCl-1.1.2-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:123c41df1db119397f2e26e9c63ca2ea853d3663e26b1c389bd3859dc1b7178a"},
{file = "PyNaCl-1.1.2-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:ceb16b7977123713ad898450ca86a2dc6706a17fe4cf278ffb6b76929c186550"},
{file = "PyNaCl-1.1.2-cp34-cp34m-win32.whl", hash = "sha256:813d4170f62d68236bb041cf731e8d1f34fc1006a5e5d81139bead6ddaa9d169"},
{file = "PyNaCl-1.1.2-cp34-cp34m-win_amd64.whl", hash = "sha256:f01405a5c453b866e35338c53882f7ba7069c1f4e4045ce67513ad45c796f8a5"},
{file = "PyNaCl-1.1.2-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:4a3be9f884df08087996516707446ba55648bbefae8428bf578fa05f20fa2ed9"},
{file = "PyNaCl-1.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7d14f18f8bc43977691276097524b9713d21b9635fea9791311261a66e4fe296"},
{file = "PyNaCl-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9532aaa446840ece574c719ad3bbf25f60ca9871f48b5446e3f73e8b498e2398"},
{file = "PyNaCl-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:67b75a950dbc4025bfa549c183baa17db4096955912f385df31830e5a2121974"},
{file = "PyNaCl-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:dfc85c2b414dee700e32764559d560063825ec1470d3ee6c973e43c80a622e56"},
{file = "PyNaCl-1.1.2-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3fd984580cbea8e02fc531aa32ab9487b72c30127f9e4c8db9ba3fe8950ecc93"},
{file = "PyNaCl-1.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:75a427377b2088c29a44db08c796c75a9cde2f9725dd041903cfbc0f6034895c"},
{file = "PyNaCl-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ca2deb968135f1400105ca902f5cef24ba6984b6a4904756498afcb9077c76f9"},
{file = "PyNaCl-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:ffb74ac578b3b80b1d2d5a23a6dd7b1d6682e5fce6a7b3d21b46b180a5546055"},
{file = "PyNaCl-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:b796d95704b674100bd99fc42bbde9f8f2ccddae8599a4d4bbcb518428dfbfed"},
{file = "PyNaCl-1.1.2.tar.gz", hash = "sha256:32f52b754abf07c319c04ce16905109cab44b0e7f7c79497431d3b2000f8af8c"},
] ]
[package.dependencies] [package.dependencies]
cffi = ">=1.4.1" cffi = ">=1.4.1"
six = "*"
[package.extras] [package.extras]
tests = ["pytest"] docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"]
[[package]] [[package]]
name = "pytest" name = "pytest"
@ -845,22 +1024,21 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]] [[package]]
name = "secret-handshake" name = "secret-handshake"
version = "0.1.0.dev3" version = "0.1.0"
description = "A module that implements Secret Handshake as specified in \"Designing a Secret Handshake: Authenticated" description = "A module that implements Secret Handshake"
optional = false optional = false
python-versions = "*" python-versions = "^3.9"
files = [ files = []
{file = "secret-handshake-0.1.0.dev3.tar.gz", hash = "sha256:be1f812101c0eb84a82a08d119090d8f423230878e233b4bfc551fb708b4e32a"}, develop = false
]
[package.dependencies] [package.dependencies]
async-generator = "1.8" PyNaCl = "^1.5.0"
pynacl = "1.1.2"
[package.extras] [package.source]
all = ["Sphinx (>=1.6.2)", "check-manifest (>=0.25)", "coverage (==4.4.1)", "isort (>=4.2.2)", "pydocstyle (==2.1.1)", "pytest (==3.4.0)", "pytest-asyncio (==0.6.0)", "pytest-cov (==2.5.1)", "pytest-mock (==1.6.3)"] type = "git"
docs = ["Sphinx (>=1.6.2)"] url = "https://gitea.polonkai.eu/gergely/PySecretHandshake"
tests = ["check-manifest (>=0.25)", "coverage (==4.4.1)", "isort (>=4.2.2)", "pydocstyle (==2.1.1)", "pytest (==3.4.0)", "pytest-asyncio (==0.6.0)", "pytest-cov (==2.5.1)", "pytest-mock (==1.6.3)"] reference = "main"
resolved_reference = "5a3af659277219536eeef4c64b8a991902f0acd2"
[[package]] [[package]]
name = "setuptools" name = "setuptools"
@ -899,17 +1077,6 @@ files = [
{file = "simplejson-3.16.0.tar.gz", hash = "sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5"}, {file = "simplejson-3.16.0.tar.gz", hash = "sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5"},
] ]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]] [[package]]
name = "snowballstemmer" name = "snowballstemmer"
version = "2.2.0" version = "2.2.0"
@ -1091,6 +1258,39 @@ files = [
{file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"}, {file = "tomlkit-0.12.1.tar.gz", hash = "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86"},
] ]
[[package]]
name = "types-pyyaml"
version = "6.0.12.12"
description = "Typing stubs for PyYAML"
optional = false
python-versions = "*"
files = [
{file = "types-PyYAML-6.0.12.12.tar.gz", hash = "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062"},
{file = "types_PyYAML-6.0.12.12-py3-none-any.whl", hash = "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24"},
]
[[package]]
name = "types-simplejson"
version = "3.19.0.2"
description = "Typing stubs for simplejson"
optional = false
python-versions = "*"
files = [
{file = "types-simplejson-3.19.0.2.tar.gz", hash = "sha256:ebc81f886f89d99d6b80c726518aa2228bc77c26438f18fd81455e4f79f8ee1b"},
{file = "types_simplejson-3.19.0.2-py3-none-any.whl", hash = "sha256:8ba093dc7884f59b3e62aed217144085e675a269debc32678fd80e0b43b2b86f"},
]
[[package]]
name = "typing-extensions"
version = "4.8.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
{file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.0.7" version = "2.0.7"
@ -1137,4 +1337,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "d61fa17ada60d932d45c104bf4d45b79411c743d3eb129f65751c8d57c4bea15" content-hash = "d80cbfdf7923c50c95505a84d8ad75eae016ca81ae32a8b22d074569b0a0fcbd"

View File

@ -8,27 +8,34 @@ readme = "README.rst"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"
async_generator = "^1.8"
PyNaCl = "^1.1.0" PyNaCl = "^1.1.0"
PyYAML = "^6.0.1" PyYAML = "^6.0.1"
secret-handshake = { version = "0.1.0.dev3", allow-prereleases = true } secret-handshake = { git = "https://gitea.polonkai.eu/gergely/PySecretHandshake", branch = "main" }
simplejson = "3.16.0" simplejson = "3.16.0"
colorlog = "^6.7.0" colorlog = "^6.7.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
check-manifest = "^0.39" check-manifest = "^0.39"
coverage = "^7.3.2" coverage = "^7.3.2"
isort = "^4.3.20" isort = "^5.12.0"
pep257 = "^0.7.0" pep257 = "^0.7.0"
pytest = "^7.4.3" pytest = "^7.4.3"
pytest-asyncio = "^0.21.1" pytest-asyncio = "^0.21.1"
pytest-cov = "^4.1.0" pytest-cov = "^4.1.0"
pytest-mock = "^3.12.0" pytest-mock = "^3.12.0"
commitizen = "^3.12.0" commitizen = "^3.12.0"
black = "^23.10.1"
pylint = "^3.0.2"
mypy = "^1.6.1"
types-pyyaml = "^6.0.12.12"
types-simplejson = "^3.19.0.2"
[tool.poetry.group.docs.dependencies] [tool.poetry.group.docs.dependencies]
Sphinx = "^2.1.1" Sphinx = "^2.1.1"
[tool.black]
line-length = 120
[tool.coverage.run] [tool.coverage.run]
branch = true branch = true
@ -38,6 +45,14 @@ skip_covered = true
fail_under = 70 fail_under = 70
omit = ["examples/*"] omit = ["examples/*"]
[tool.isort]
force_sort_within_sections = true
line_length = 120
profile = "black"
[tool.pylint.format]
max-line-length = 120
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = ["--cov=.", "--no-cov-on-fail"] addopts = ["--cov=.", "--no-cov-on-fail"]
python_files = ["tests/test_*.py"] python_files = ["tests/test_*.py"]

View File

@ -1,3 +1,5 @@
from .models import Feed, LocalFeed, Message, LocalMessage, NoPrivateKeyException """Feed related functionality"""
__all__ = ('Feed', 'LocalFeed', 'Message', 'LocalMessage', 'NoPrivateKeyException') from .models import Feed, LocalFeed, LocalMessage, Message, NoPrivateKeyException
__all__ = ("Feed", "LocalFeed", "Message", "LocalMessage", "NoPrivateKeyException")

View File

@ -1,108 +1,172 @@
import datetime """Feed models"""
from base64 import b64encode
from collections import namedtuple, OrderedDict
from hashlib import sha256
from base64 import b64encode
from collections import OrderedDict, namedtuple
import datetime
from hashlib import sha256
from typing import Any, Dict, Optional
from nacl.signing import SigningKey, VerifyKey
from simplejson import dumps, loads from simplejson import dumps, loads
from typing_extensions import Self
from ssb.util import tag from ssb.util import tag
OrderedMsg = namedtuple("OrderedMsg", ("previous", "author", "sequence", "timestamp", "hash", "content"))
OrderedMsg = namedtuple('OrderedMsg', ('previous', 'author', 'sequence', 'timestamp', 'hash', 'content'))
class NoPrivateKeyException(Exception): class NoPrivateKeyException(Exception):
pass """Exception to raise when a private key is not available"""
def to_ordered(data): def to_ordered(data: Dict[str, Any]) -> OrderedDict[str, Any]:
"""Convert a dictionary to an ``OrderedDict``"""
smsg = OrderedMsg(**data) smsg = OrderedMsg(**data)
return OrderedDict((k, getattr(smsg, k)) for k in smsg._fields) return OrderedDict((k, getattr(smsg, k)) for k in smsg._fields)
def get_millis_1970(): def get_millis_1970() -> int:
"""Get the UNIX timestamp in milliseconds"""
return int(datetime.datetime.utcnow().timestamp() * 1000) return int(datetime.datetime.utcnow().timestamp() * 1000)
class Feed(object): class Feed:
def __init__(self, public_key): """Base class for feeds"""
def __init__(self, public_key: VerifyKey):
self.public_key = public_key self.public_key = public_key
@property @property
def id(self): def id(self) -> str:
return tag(self.public_key).decode('ascii') """The identifier of the feed"""
def sign(self, msg): return tag(self.public_key).decode("ascii")
raise NoPrivateKeyException('Cannot use remote identity to sign (no private key!)')
def sign(self, msg: "Message") -> bytes:
"""Sign a message"""
raise NoPrivateKeyException("Cannot use remote identity to sign (no private key!)")
class LocalFeed(Feed): class LocalFeed(Feed):
def __init__(self, private_key): """Class representing a local feed"""
self.private_key = private_key
def __init__(self, private_key: SigningKey): # pylint: disable=super-init-not-called
self.private_key: SigningKey = private_key
@property @property
def public_key(self): def public_key(self) -> VerifyKey:
"""The public key of the feed"""
return self.private_key.verify_key return self.private_key.verify_key
def sign(self, msg): @public_key.setter
def public_key(self, _: VerifyKey) -> None:
raise TypeError("Cannot set just the public key of a local feed")
def sign(self, msg: "Message") -> bytes:
"""Sign a message for this feed"""
return self.private_key.sign(msg).signature return self.private_key.sign(msg).signature
class Message(object): class Message:
def __init__(self, feed, content, signature, sequence=1, timestamp=None, previous=None): """Base class for SSB messages"""
def __init__( # pylint: disable=too-many-arguments
self,
feed: Feed,
content: Dict[str, Any],
signature: Optional[str] = None,
sequence: int = 1,
timestamp: Optional[int] = None,
previous: Optional["Message"] = None,
):
self.feed = feed self.feed = feed
self.content = content self.content = content
if signature is None: if signature is None:
raise ValueError("signature can't be None") raise ValueError("signature can't be None")
self.signature = signature self.signature = signature
self.previous = previous self.previous = previous
if self.previous: if self.previous:
self.sequence = self.previous.sequence + 1 self.sequence: int = self.previous.sequence + 1
else: else:
self.sequence = sequence self.sequence = sequence
self.timestamp = get_millis_1970() if timestamp is None else timestamp self.timestamp = get_millis_1970() if timestamp is None else timestamp
@classmethod @classmethod
def parse(cls, data, feed): def parse(cls, data: bytes, feed: Feed) -> Self:
"""Parse raw message data"""
obj = loads(data, object_pairs_hook=OrderedDict) obj = loads(data, object_pairs_hook=OrderedDict)
msg = cls(feed, obj['content'], timestamp=obj['timestamp']) msg = cls(feed, obj["content"], timestamp=obj["timestamp"])
return msg return msg
def serialize(self, add_signature=True): def serialize(self, add_signature: bool = True) -> bytes:
return dumps(self.to_dict(add_signature=add_signature), indent=2).encode('utf-8') """Serialize the message"""
def to_dict(self, add_signature=True): return dumps(self.to_dict(add_signature=add_signature), indent=2).encode("utf-8")
obj = to_ordered({
'previous': self.previous.key if self.previous else None, def to_dict(self, add_signature: bool = True) -> OrderedDict[str, Any]:
'author': self.feed.id, """Convert the message to a dictionary"""
'sequence': self.sequence,
'timestamp': self.timestamp, obj = to_ordered(
'hash': 'sha256', {
'content': self.content "previous": self.previous.key if self.previous else None,
}) "author": self.feed.id,
"sequence": self.sequence,
"timestamp": self.timestamp,
"hash": "sha256",
"content": self.content,
}
)
if add_signature: if add_signature:
obj['signature'] = self.signature obj["signature"] = self.signature
return obj return obj
def verify(self, signature): def verify(self, signature: str) -> bool:
"""Verify the signature of the message"""
return self.signature == signature return self.signature == signature
@property @property
def hash(self): def hash(self) -> str:
hash = sha256(self.serialize()).digest() """The cryptographic hash of the message"""
return b64encode(hash).decode('ascii') + '.sha256'
hash_ = sha256(self.serialize()).digest()
return b64encode(hash_).decode("ascii") + ".sha256"
@property @property
def key(self): def key(self) -> str:
return '%' + self.hash """The key of the message"""
return "%" + self.hash
class LocalMessage(Message): class LocalMessage(Message):
def __init__(self, feed, content, signature=None, sequence=1, timestamp=None, previous=None): """Class representing a local message"""
def __init__( # pylint: disable=too-many-arguments,super-init-not-called
self,
feed: Feed,
content: Dict[str, Any],
signature: Optional[str] = None,
sequence: int = 1,
timestamp: Optional[int] = None,
previous: Optional[Message] = None,
):
self.feed = feed self.feed = feed
self.content = content self.content = content
@ -119,7 +183,8 @@ class LocalMessage(Message):
else: else:
self.signature = signature self.signature = signature
def _sign(self): def _sign(self) -> str:
# ensure ordering of keys and indentation of 2 characters, like ssb-keys # ensure ordering of keys and indentation of 2 characters, like ssb-keys
data = self.serialize(add_signature=False) data = self.serialize(add_signature=False)
return (b64encode(bytes(self.feed.sign(data))) + b'.sig.ed25519').decode('ascii')
return (b64encode(bytes(self.feed.sign(data))) + b".sig.ed25519").decode("ascii")

View File

@ -1,140 +1,238 @@
from functools import wraps """MuxRPC"""
from async_generator import async_generator, yield_ from typing import Any, AsyncIterator, Callable, Dict, Generator, List, Literal, Optional, Union
from ssb.packet_stream import PSMessageType from typing_extensions import Self
from .packet_stream import PacketStream, PSMessage, PSMessageType, PSRequestHandler, PSStreamHandler
MuxRPCJSON = Dict[str, Any]
MuxRPCCallType = Literal["async", "duplex", "sink", "source", "sync"]
MuxRPCRequestHandlerType = Callable[[PacketStream, "MuxRPCRequest"], None]
MuxRPCRequestParam = Union[bytes, str, MuxRPCJSON] # pylint: disable=invalid-name
class MuxRPCAPIException(Exception): class MuxRPCAPIException(Exception):
pass """Exception to raise on MuxRPC API errors"""
class MuxRPCHandler(object): class MuxRPCHandler: # pylint: disable=too-few-public-methods
def check_message(self, msg): """Base MuxRPC handler class"""
def check_message(self, msg: PSMessage) -> None:
"""Check message validity"""
body = msg.body body = msg.body
if isinstance(body, dict) and 'name' in body and body['name'] == 'Error':
raise MuxRPCAPIException(body['message']) if isinstance(body, dict) and "name" in body and body["name"] == "Error":
raise MuxRPCAPIException(body["message"])
def __await__(self) -> Generator[Optional[PSMessage], None, None]:
raise NotImplementedError()
def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
raise NotImplementedError()
async def __anext__(self) -> Optional[PSMessage]:
raise NotImplementedError()
def send(self, msg: Any, msg_type: PSMessageType = PSMessageType.JSON, end: bool = False) -> None:
"""Send a message through the stream"""
raise NotImplementedError()
class MuxRPCRequestHandler(MuxRPCHandler): class MuxRPCRequestHandler(MuxRPCHandler): # pylint: disable=abstract-method
def __init__(self, ps_handler): """Base class for MuxRPC request handlers"""
def __init__(self, ps_handler: PSRequestHandler):
self.ps_handler = ps_handler self.ps_handler = ps_handler
def __await__(self): def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
msg = (yield from self.ps_handler.__await__()) return self
async def __anext__(self) -> Optional[PSMessage]:
msg = await self.ps_handler.__anext__()
assert msg
self.check_message(msg) self.check_message(msg)
return msg return msg
class MuxRPCSourceHandler(MuxRPCHandler): class MuxRPCSourceHandler(MuxRPCHandler): # pylint: disable=abstract-method
def __init__(self, ps_handler): """MuxRPC handler for sources"""
def __init__(self, ps_handler: PSStreamHandler):
self.ps_handler = ps_handler self.ps_handler = ps_handler
@async_generator def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
async def __aiter__(self): return self
async for msg in self.ps_handler:
try: async def __anext__(self) -> Optional[PSMessage]:
msg = await self.ps_handler.__anext__()
assert msg
self.check_message(msg) self.check_message(msg)
await yield_(msg)
except MuxRPCAPIException: return msg
raise
class MuxRPCSinkHandlerMixin(object): class MuxRPCSinkHandlerMixin: # pylint: disable=too-few-public-methods
"""Mixin for sink-type MuxRPC handlers"""
connection: Optional[PacketStream]
req: Optional[int]
def send(self, msg: Any, msg_type: PSMessageType = PSMessageType.JSON, end: bool = False) -> None:
"""Send a message through the stream"""
assert self.connection
def send(self, msg, msg_type=PSMessageType.JSON, end=False):
self.connection.send(msg, stream=True, msg_type=msg_type, req=self.req, end_err=end) self.connection.send(msg, stream=True, msg_type=msg_type, req=self.req, end_err=end)
class MuxRPCDuplexHandler(MuxRPCSinkHandlerMixin, MuxRPCSourceHandler): class MuxRPCDuplexHandler(MuxRPCSinkHandlerMixin, MuxRPCSourceHandler): # pylint: disable=abstract-method
def __init__(self, ps_handler, connection, req): """MuxRPC handler for duplex streams"""
super(MuxRPCDuplexHandler, self).__init__(ps_handler)
def __init__(self, ps_handler: PSStreamHandler, connection: PacketStream, req: int):
super().__init__(ps_handler)
self.connection = connection self.connection = connection
self.req = req self.req = req
class MuxRPCSinkHandler(MuxRPCHandler, MuxRPCSinkHandlerMixin): class MuxRPCSinkHandler(MuxRPCHandler, MuxRPCSinkHandlerMixin): # pylint: disable=abstract-method
def __init__(self, connection, req): """MuxRPC handler for sinks"""
def __init__(self, connection: PacketStream, req: int):
self.connection = connection self.connection = connection
self.req = req self.req = req
def _get_appropriate_api_handler(type_, connection, ps_handler, req): def _get_appropriate_api_handler(
if type_ in {'sync', 'async'}: type_: MuxRPCCallType, connection: PacketStream, ps_handler: Union[PSRequestHandler, PSStreamHandler], req: int
) -> MuxRPCHandler:
"""Find the appropriate MuxRPC handler"""
if type_ in {"sync", "async"}:
assert isinstance(ps_handler, PSRequestHandler)
return MuxRPCRequestHandler(ps_handler) return MuxRPCRequestHandler(ps_handler)
elif type_ == 'source':
if type_ == "source":
assert isinstance(ps_handler, PSStreamHandler)
return MuxRPCSourceHandler(ps_handler) return MuxRPCSourceHandler(ps_handler)
elif type_ == 'sink':
if type_ == "sink":
return MuxRPCSinkHandler(connection, req) return MuxRPCSinkHandler(connection, req)
elif type_ == 'duplex':
if type_ == "duplex":
assert isinstance(ps_handler, PSStreamHandler)
return MuxRPCDuplexHandler(ps_handler, connection, req) return MuxRPCDuplexHandler(ps_handler, connection, req)
raise TypeError(f"Unknown request type {type_}")
class MuxRPCRequest:
"""MuxRPC request"""
class MuxRPCRequest(object):
@classmethod @classmethod
def from_message(cls, message): def from_message(cls, message: PSMessage) -> Self:
body = message.body """Initialise a request from a raw packet stream message"""
return cls('.'.join(body['name']), body['args'])
def __init__(self, name, args): body = message.body
return cls(".".join(body["name"]), body["args"])
def __init__(self, name: str, args: List[MuxRPCRequestParam]):
self.name = name self.name = name
self.args = args self.args = args
def __repr__(self): def __repr__(self) -> str:
return '<MuxRPCRequest {0.name} {0.args}>'.format(self) return f"<MuxRPCRequest {self.name} {self.args}>"
class MuxRPCMessage(object): class MuxRPCMessage:
"""MuxRPC message"""
@classmethod @classmethod
def from_message(cls, message): def from_message(cls, message: PSMessage) -> Self:
"""Initialise a MuxRPC message from a raw packet stream message"""
return cls(message.body) return cls(message.body)
def __init__(self, body): def __init__(self, body: PSMessage):
self.body = body self.body = body
def __repr__(self): def __repr__(self) -> str:
return '<MuxRPCMessage {0.body}}>'.format(self) return f"<MuxRPCMessage {self.body}>"
class MuxRPCAPI(object): class MuxRPCAPI:
def __init__(self): """Generit MuxRPC API"""
self.handlers = {}
self.connection = None def __init__(self) -> None:
self.handlers: Dict[str, MuxRPCRequestHandlerType] = {}
self.connection: Optional[PacketStream] = None
def __aiter__(self) -> AsyncIterator[None]:
return self
async def __anext__(self) -> None:
assert self.connection
req_message = await self.connection.__anext__()
async def __await__(self):
async for req_message in self.connection:
body = req_message.body
if req_message is None: if req_message is None:
return raise StopAsyncIteration()
if isinstance(body, dict) and body.get('name'):
body = req_message.body
if isinstance(body, dict) and body.get("name"):
self.process(self.connection, MuxRPCRequest.from_message(req_message)) self.process(self.connection, MuxRPCRequest.from_message(req_message))
def add_connection(self, connection): def __await__(self) -> Generator[None, None, None]:
yield from self.__anext__().__await__()
def add_connection(self, connection: PacketStream) -> None:
"""Set the packet stream connection of this RPC API"""
self.connection = connection self.connection = connection
def define(self, name): def define(self, name: str) -> Callable[[MuxRPCRequestHandlerType], MuxRPCRequestHandlerType]:
def _handle(f): """Decorator to define an RPC method handler"""
def _handle(f: MuxRPCRequestHandlerType) -> MuxRPCRequestHandlerType:
self.handlers[name] = f self.handlers[name] = f
@wraps(f)
def _f(*args, **kwargs):
return f(*args, **kwargs)
return f return f
return _handle return _handle
def process(self, connection, request): def process(self, connection: PacketStream, request: MuxRPCRequest) -> None:
"""Process an incoming request"""
handler = self.handlers.get(request.name) handler = self.handlers.get(request.name)
if not handler: if not handler:
raise MuxRPCAPIException('Method {} not found!'.format(request.name)) raise MuxRPCAPIException(f"Method {request.name} not found!")
handler(connection, request) handler(connection, request)
def call(self, name, args, type_='sync'): def call(self, name: str, args: List[MuxRPCRequestParam], type_: MuxRPCCallType = "sync") -> MuxRPCHandler:
"""Call an RPC method"""
assert self.connection
if not self.connection.is_connected: if not self.connection.is_connected:
raise Exception('not connected') raise Exception("not connected") # pylint: disable=broad-exception-raised
old_counter = self.connection.req_counter old_counter = self.connection.req_counter
ps_handler = self.connection.send({ ps_handler = self.connection.send(
'name': name.split('.'), {"name": name.split("."), "args": args, "type": type_},
'args': args, stream=type_ in {"sink", "source", "duplex"},
'type': type_ )
}, stream=type_ in {'sink', 'source', 'duplex'})
return _get_appropriate_api_handler(type_, self.connection, ps_handler, old_counter) return _get_appropriate_api_handler(type_, self.connection, ps_handler, old_counter)

View File

@ -1,180 +1,255 @@
import logging """Packet streams"""
import struct
from asyncio import Event, Queue from asyncio import Event, Queue
from enum import Enum from enum import Enum
from time import time import logging
from math import ceil from math import ceil
import struct
from time import time
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Union
from secret_handshake.network import SHSDuplexStream
import simplejson import simplejson
from async_generator import async_generator, yield_ from typing_extensions import Self
from secret_handshake import SHSClient, SHSServer PSHandler = Union["PSRequestHandler", "PSStreamHandler"]
PSMessageData = Union[bytes, bool, Dict[str, Any], str]
logger = logging.getLogger("packet_stream")
logger = logging.getLogger('packet_stream')
class PSMessageType(Enum): class PSMessageType(Enum):
"""Available message types"""
BUFFER = 0 BUFFER = 0
TEXT = 1 TEXT = 1
JSON = 2 JSON = 2
class PSStreamHandler(object): class PSStreamHandler:
def __init__(self, req): """Packet stream handler"""
super(PSStreamHandler).__init__()
self.req = req def __init__(self, req: int):
self.queue = Queue() self.req = req
self.queue: Queue[Optional["PSMessage"]] = Queue()
async def process(self, msg: "PSMessage") -> None:
"""Process a pending message"""
async def process(self, msg):
await self.queue.put(msg) await self.queue.put(msg)
async def stop(self): async def stop(self) -> None:
"""Stop a pending request"""
await self.queue.put(None) await self.queue.put(None)
@async_generator def __aiter__(self) -> AsyncIterator[Optional["PSMessage"]]:
async def __aiter__(self): return self
while True:
async def __anext__(self) -> Optional["PSMessage"]:
elem = await self.queue.get() elem = await self.queue.get()
if not elem: if not elem:
return raise StopAsyncIteration()
await yield_(elem)
return elem
class PSRequestHandler(object): class PSRequestHandler:
def __init__(self, req): """Packet stream request handler"""
super(PSRequestHandler).__init__()
def __init__(self, req: int):
self.req = req self.req = req
self.event = Event() self.event = Event()
self._msg = None self._msg: Optional[PSMessage] = None
async def process(self, msg: "PSMessage") -> None:
"""Process a message request"""
async def process(self, msg):
self._msg = msg self._msg = msg
self.event.set() self.event.set()
async def stop(self): async def stop(self) -> None:
"""Stop a pending event request"""
if not self.event.is_set(): if not self.event.is_set():
self.event.set() self.event.set()
def __await__(self): def __aiter__(self):
return self
async def __anext__(self) -> Optional["PSMessage"]:
# wait until 'process' is called # wait until 'process' is called
yield from self.event.wait().__await__() await self.event.wait()
return self._msg return self._msg
class PSMessage(object): class PSMessage:
"""Packet Stream message"""
@classmethod @classmethod
def from_header_body(cls, flags, req, body): def from_header_body(cls, flags: int, req: int, body: bytes) -> Self:
"""Parse a raw message"""
type_ = PSMessageType(flags & 0x03) type_ = PSMessageType(flags & 0x03)
if type_ == PSMessageType.TEXT: if type_ == PSMessageType.TEXT:
body = body.decode('utf-8') body_s = body.decode("utf-8")
elif type_ == PSMessageType.JSON: elif type_ == PSMessageType.JSON:
body = simplejson.loads(body) body_s = simplejson.loads(body)
return cls(type_, body, bool(flags & 0x08), bool(flags & 0x04), req=req) return cls(type_, body_s, bool(flags & 0x08), bool(flags & 0x04), req=req)
@property @property
def data(self): def data(self) -> bytes:
"""The raw message data"""
if self.type == PSMessageType.TEXT: if self.type == PSMessageType.TEXT:
return self.body.encode('utf-8') assert isinstance(self.body, str)
elif self.type == PSMessageType.JSON:
return simplejson.dumps(self.body).encode('utf-8') return self.body.encode("utf-8")
if self.type == PSMessageType.JSON:
return simplejson.dumps(self.body).encode("utf-8")
assert isinstance(self.body, bytes)
return self.body return self.body
def __init__(self, type_, body, stream, end_err, req=None): def __init__(
self, type_: PSMessageType, body: Any, stream: bool, end_err: bool, req: Optional[int] = None
): # pylint: disable=too-many-arguments
self.stream = stream self.stream = stream
self.end_err = end_err self.end_err = end_err
self.type = type_ self.type = type_
self.body = body self.body = body
self.req = req self.req = req
def __repr__(self): def __repr__(self) -> str:
if self.type == PSMessageType.BUFFER: if self.type == PSMessageType.BUFFER:
body = '{} bytes'.format(len(self.body)) body = f"{len(self.body)} bytes"
else: else:
body = self.body body = self.body
return '<PSMessage ({}): {}{} {}{}>'.format(self.type.name, body,
'' if self.req is None else ' [{}]'.format(self.req), req = "" if self.req is None else f" [{self.req}]"
'~' if self.stream else '', '!' if self.end_err else '') is_stream = "~" if self.stream else ""
err = "!" if self.end_err else ""
return f"<PSMessage ({self.type.name}): {body}{req} {is_stream}{err}>"
class PacketStream(object): class PacketStream:
def __init__(self, connection): """SSB Packet stream"""
def __init__(self, connection: SHSDuplexStream):
self.connection = connection self.connection = connection
self.req_counter = 1 self.req_counter = 1
self._event_map = {} self._event_map: Dict[int, Tuple[float, PSHandler]] = {}
self._connected = False
def register_handler(self, handler: PSHandler) -> None:
"""Register an RPC handler"""
def register_handler(self, handler):
self._event_map[handler.req] = (time(), handler) self._event_map[handler.req] = (time(), handler)
@property @property
def is_connected(self): def is_connected(self) -> bool:
"""Check if the stream is connected"""
return self.connection.is_connected return self.connection.is_connected
@async_generator def __aiter__(self) -> AsyncIterator[Optional[PSMessage]]:
async def __aiter__(self): return self
while True:
msg = await self.read()
if not msg:
return
# filter out replies
if msg.req >= 0:
await yield_(msg)
async def __await__(self): async def __anext__(self) -> Optional[PSMessage]:
msg = await self.read()
if not msg:
raise StopAsyncIteration()
if msg.req is not None and msg.req >= 0:
return msg
return None
async def __await__(self) -> None:
async for data in self: async for data in self:
logger.info('RECV: %r', data) logger.info("RECV: %r", data)
if data is None: if data is None:
return return
async def _read(self): async def _read(self) -> Optional[PSMessage]:
try: try:
header = await self.connection.read() header = await self.connection.read()
if not header or header == b'\x00' * 9:
return if not header or header == b"\x00" * 9:
flags, length, req = struct.unpack('>BIi', header) return None
flags, length, req = struct.unpack(">BIi", header)
n_packets = ceil(length / 4096) n_packets = ceil(length / 4096)
body = b'' body = b""
for n in range(n_packets):
body += await self.connection.read() for _ in range(n_packets):
read_data = await self.connection.read()
if read_data is not None:
body += read_data
logger.debug("READ %s %s", header, len(body))
logger.debug('READ %s %s', header, len(body))
return PSMessage.from_header_body(flags, req, body) return PSMessage.from_header_body(flags, req, body)
except StopAsyncIteration: except StopAsyncIteration:
logger.debug('DISCONNECT') logger.debug("DISCONNECT")
self.connection.disconnect() self.connection.disconnect()
return None return None
async def read(self): async def read(self) -> Optional[PSMessage]:
"""Read data from the packet stream"""
msg = await self._read() msg = await self._read()
if not msg: if not msg:
return None return None
# check whether it's a reply and handle accordingly # check whether it's a reply and handle accordingly
if msg.req < 0: if msg.req is not None and msg.req < 0:
t, handler = self._event_map[-msg.req] _, handler = self._event_map[-msg.req]
await handler.process(msg) await handler.process(msg)
logger.info('RESPONSE [%d]: %r', -msg.req, msg) logger.info("RESPONSE [%d]: %r", -msg.req, msg)
if msg.end_err: if msg.end_err:
await handler.stop() await handler.stop()
del self._event_map[-msg.req] del self._event_map[-msg.req]
logger.info('RESPONSE [%d]: EOS', -msg.req) logger.info("RESPONSE [%d]: EOS", -msg.req)
return msg return msg
def _write(self, msg): def _write(self, msg: PSMessage) -> None:
logger.info('SEND [%d]: %r', msg.req, msg) logger.info("SEND [%d]: %r", msg.req, msg)
header = struct.pack('>BIi', (int(msg.stream) << 3) | (int(msg.end_err) << 2) | msg.type.value, len(msg.data), header = struct.pack(
msg.req) ">BIi", (int(msg.stream) << 3) | (int(msg.end_err) << 2) | msg.type.value, len(msg.data), msg.req
)
self.connection.write(header) self.connection.write(header)
self.connection.write(msg.data) self.connection.write(msg.data)
logger.debug('WRITE HDR: %s', header) logger.debug("WRITE HDR: %s", header)
logger.debug('WRITE DATA: %s', msg.data) logger.debug("WRITE DATA: %s", msg.data)
def send( # pylint: disable=too-many-arguments
self,
data: Any,
msg_type: PSMessageType = PSMessageType.JSON,
stream: bool = False,
end_err: bool = False,
req: Optional[int] = None,
) -> PSHandler:
"""Send data through the packet stream"""
def send(self, data, msg_type=PSMessageType.JSON, stream=False, end_err=False, req=None):
update_counter = False update_counter = False
if req is None: if req is None:
update_counter = True update_counter = True
req = self.req_counter req = self.req_counter
@ -185,15 +260,19 @@ class PacketStream(object):
self._write(msg) self._write(msg)
if stream: if stream:
handler = PSStreamHandler(self.req_counter) handler: PSHandler = PSStreamHandler(self.req_counter)
else: else:
handler = PSRequestHandler(self.req_counter) handler = PSRequestHandler(self.req_counter)
self.register_handler(handler) self.register_handler(handler)
if update_counter: if update_counter:
self.req_counter += 1 self.req_counter += 1
return handler return handler
def disconnect(self): def disconnect(self) -> None:
"""Disconnect the stream"""
self._connected = False self._connected = False
self.connection.disconnect() self.connection.disconnect()

View File

@ -1,29 +1,39 @@
import os """Utility functions"""
import yaml
from base64 import b64decode, b64encode
from nacl.signing import SigningKey from base64 import b64decode, b64encode
import os
from typing import TypedDict
from nacl.signing import SigningKey, VerifyKey
import yaml
class SSBSecret(TypedDict):
"""Dictionary type to hold an SSB secret identity"""
keypair: SigningKey
id: str
class ConfigException(Exception): class ConfigException(Exception):
pass """Exception to raise if there is a problem with the configuration data"""
def tag(key): def tag(key: VerifyKey) -> bytes:
"""Create tag from publick key.""" """Create tag from public key."""
return b'@' + b64encode(bytes(key)) + b'.ed25519'
return b"@" + b64encode(bytes(key)) + b".ed25519"
def load_ssb_secret(): def load_ssb_secret() -> SSBSecret:
"""Load SSB keys from ~/.ssb""" """Load SSB keys from ~/.ssb"""
with open(os.path.expanduser('~/.ssb/secret')) as f:
with open(os.path.expanduser("~/.ssb/secret"), encoding="utf-8") as f:
config = yaml.load(f, Loader=yaml.SafeLoader) config = yaml.load(f, Loader=yaml.SafeLoader)
if config['curve'] != 'ed25519': if config["curve"] != "ed25519":
raise ConfigException('Algorithm not known: ' + config['curve']) raise ConfigException("Algorithm not known: " + config["curve"])
server_prv_key = b64decode(config['private'][:-8]) server_prv_key = b64decode(config["private"][:-8])
return {
'keypair': SigningKey(server_prv_key[:32]), return {"keypair": SigningKey(server_prv_key[:32]), "id": config["id"]}
'id': config['id']
}

View File

@ -1,11 +1,12 @@
"""Tests for the feed functionality"""
from base64 import b64decode from base64 import b64decode
from collections import OrderedDict from collections import OrderedDict
import pytest
from nacl.signing import SigningKey, VerifyKey from nacl.signing import SigningKey, VerifyKey
import pytest
from ssb.feed import LocalMessage, LocalFeed, Feed, Message, NoPrivateKeyException from ssb.feed import Feed, LocalFeed, LocalMessage, Message, NoPrivateKeyException
SERIALIZED_M1 = b"""{ SERIALIZED_M1 = b"""{
"previous": null, "previous": null,
@ -23,127 +24,147 @@ SERIALIZED_M1 = b"""{
}""" }"""
@pytest.fixture() @pytest.fixture
def local_feed(): def local_feed() -> LocalFeed:
secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=') """Fixture providing a local feed"""
secret = b64decode("Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=")
return LocalFeed(SigningKey(secret)) return LocalFeed(SigningKey(secret))
@pytest.fixture() @pytest.fixture
def remote_feed(): def remote_feed() -> Feed:
public = b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=') """Fixture providing a remote feed"""
public = b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
return Feed(VerifyKey(public)) return Feed(VerifyKey(public))
def test_local_feed(): def test_local_feed() -> None:
secret = b64decode('Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=') """Test a local feed"""
secret = b64decode("Mz2qkNOP2K6upnqibWrR+z8pVUI1ReA1MLc7QMtF2qQ=")
feed = LocalFeed(SigningKey(secret)) feed = LocalFeed(SigningKey(secret))
assert bytes(feed.private_key) == secret assert bytes(feed.private_key) == secret
assert bytes(feed.public_key) == b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=') assert bytes(feed.public_key) == b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
assert feed.id == '@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519' assert feed.id == "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519"
def test_remote_feed(): def test_remote_feed() -> None:
public = b64decode('I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=') """Test a remote feed"""
public = b64decode("I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=")
feed = Feed(VerifyKey(public)) feed = Feed(VerifyKey(public))
assert bytes(feed.public_key) == public assert bytes(feed.public_key) == public
assert feed.id == '@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519' assert feed.id == "@I/4cyN/jPBbDsikbHzAEvmaYlaJK33lW3UhWjNXjyrU=.ed25519"
m1 = Message(feed, OrderedDict([ m1 = Message(
('type', 'about'), feed,
('about', feed.id), OrderedDict([("type", "about"), ("about", feed.id), ("name", "neo"), ("description", "The Chosen One")]),
('name', 'neo'), "foo",
('description', 'The Chosen One') timestamp=1495706260190,
]), 'foo', timestamp=1495706260190) )
with pytest.raises(NoPrivateKeyException): with pytest.raises(NoPrivateKeyException):
feed.sign(m1) feed.sign(m1)
def test_local_message(local_feed): def test_local_message(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
m1 = LocalMessage(local_feed, OrderedDict([ """Test a local message"""
('type', 'about'),
('about', local_feed.id), m1 = LocalMessage(
('name', 'neo'), local_feed,
('description', 'The Chosen One') OrderedDict([("type", "about"), ("about", local_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
]), timestamp=1495706260190) timestamp=1495706260190,
)
assert m1.timestamp == 1495706260190 assert m1.timestamp == 1495706260190
assert m1.previous is None assert m1.previous is None
assert m1.sequence == 1 assert m1.sequence == 1
assert m1.signature == \ assert m1.signature == (
'lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519' "lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
assert m1.key == '%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256' )
assert m1.key == "%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256"
m2 = LocalMessage(local_feed, OrderedDict([ m2 = LocalMessage(
('type', 'about'), local_feed,
('about', local_feed.id), OrderedDict(
('name', 'morpheus'), [("type", "about"), ("about", local_feed.id), ("name", "morpheus"), ("description", "Dude with big jaw")]
('description', 'Dude with big jaw') ),
]), previous=m1, timestamp=1495706447426) previous=m1,
timestamp=1495706447426,
)
assert m2.timestamp == 1495706447426 assert m2.timestamp == 1495706447426
assert m2.previous is m1 assert m2.previous is m1
assert m2.sequence == 2 assert m2.sequence == 2
assert m2.signature == \ assert m2.signature == (
'3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519' "3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
assert m2.key == '%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256' )
assert m2.key == "%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256"
def test_remote_message(remote_feed): def test_remote_message(remote_feed: Feed) -> None: # pylint: disable=redefined-outer-name
signature = 'lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519' """Test a remote message"""
m1 = Message(remote_feed, OrderedDict([
('type', 'about'), signature = "lPsQ9P10OgeyH6u0unFgiI2wV/RQ7Q2x2ebxnXYCzsJ055TBMXphRADTKhOMS2EkUxXQ9k3amj5fnWPudGxwBQ==.sig.ed25519"
('about', remote_feed.id), m1 = Message(
('name', 'neo'), remote_feed,
('description', 'The Chosen One') OrderedDict([("type", "about"), ("about", remote_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
]), signature, timestamp=1495706260190) signature,
timestamp=1495706260190,
)
assert m1.timestamp == 1495706260190 assert m1.timestamp == 1495706260190
assert m1.previous is None assert m1.previous is None
assert m1.sequence == 1 assert m1.sequence == 1
assert m1.signature == signature assert m1.signature == signature
assert m1.key == '%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256' assert m1.key == "%xRDqws/TrQmOd4aEwZ32jdLhP873ZKjIgHlggPR0eoo=.sha256"
signature = '3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519' signature = "3SY85LX6/ppOfP4SbfwZbKfd6DccbLRiB13pwpzbSK0nU52OEJxOqcJ2Uensr6RkrWztWLIq90sNOn1zRAoOAw==.sig.ed25519"
m2 = Message(remote_feed, OrderedDict([ m2 = Message(
('type', 'about'), remote_feed,
('about', remote_feed.id), OrderedDict(
('name', 'morpheus'), [("type", "about"), ("about", remote_feed.id), ("name", "morpheus"), ("description", "Dude with big jaw")]
('description', 'Dude with big jaw') ),
]), signature, previous=m1, timestamp=1495706447426) signature,
previous=m1,
timestamp=1495706447426,
)
assert m2.timestamp == 1495706447426 assert m2.timestamp == 1495706447426
assert m2.previous is m1 assert m2.previous is m1
assert m2.sequence == 2 assert m2.sequence == 2
assert m2.signature == signature assert m2.signature == signature
m2.verify(signature) m2.verify(signature)
assert m2.key == '%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256' assert m2.key == "%nx13uks5GUwuKJC49PfYGMS/1pgGTtwwdWT7kbVaroM=.sha256"
def test_remote_no_signature(remote_feed): def test_remote_no_signature(remote_feed: Feed) -> None: # pylint: disable=redefined-outer-name
"""Test remote feed without a signature"""
with pytest.raises(ValueError): with pytest.raises(ValueError):
Message(remote_feed, OrderedDict([ Message(
('type', 'about'), remote_feed,
('about', remote_feed.id), OrderedDict(
('name', 'neo'), [("type", "about"), ("about", remote_feed.id), ("name", "neo"), ("description", "The Chosen One")]
('description', 'The Chosen One') ),
]), None, timestamp=1495706260190) None,
timestamp=1495706260190,
)
def test_serialize(local_feed): def test_serialize(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
m1 = LocalMessage(local_feed, OrderedDict([ """Test feed serialization"""
('type', 'about'),
('about', local_feed.id), m1 = LocalMessage(
('name', 'neo'), local_feed,
('description', 'The Chosen One') OrderedDict([("type", "about"), ("about", local_feed.id), ("name", "neo"), ("description", "The Chosen One")]),
]), timestamp=1495706260190) timestamp=1495706260190,
)
assert m1.serialize() == SERIALIZED_M1 assert m1.serialize() == SERIALIZED_M1
def test_parse(local_feed): def test_parse(local_feed: LocalFeed) -> None: # pylint: disable=redefined-outer-name
"""Test feed parsing"""
m1 = LocalMessage.parse(SERIALIZED_M1, local_feed) m1 = LocalMessage.parse(SERIALIZED_M1, local_feed)
assert m1.content == { assert m1.content == {"type": "about", "about": local_feed.id, "name": "neo", "description": "The Chosen One"}
'type': 'about',
'about': local_feed.id,
'name': 'neo',
'description': 'The Chosen One'
}
assert m1.timestamp == 1495706260190 assert m1.timestamp == 1495706260190

View File

@ -1,96 +1,140 @@
"""Tests for the packet stream"""
from asyncio import Event, ensure_future, gather
from asyncio.events import AbstractEventLoop
import json import json
from asyncio import ensure_future, gather, Event from typing import AsyncGenerator, Awaitable, Callable, Generator, List
import pytest import pytest
from nacl.signing import SigningKey from pytest_mock import MockerFixture
from secret_handshake.network import SHSDuplexStream from secret_handshake.network import SHSDuplexStream
from ssb.packet_stream import PacketStream, PSMessageType
from ssb.packet_stream import PacketStream, PSMessage, PSMessageType
async def _collect_messages(generator): async def _collect_messages(generator: AsyncGenerator[PSMessage, None]) -> List[PSMessage]:
results = [] results = []
async for msg in generator: async for msg in generator:
results.append(msg) results.append(msg)
return results return results
MSG_BODY_1 = (b'{"previous":"%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA'
MSG_BODY_1 = (
b'{"previous":"%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA'
b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":116,"timestamp":1496696699331,"hash":"sha256","content":{"type"' b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":116,"timestamp":1496696699331,"hash":"sha256","content":{"type"'
b':"post","channel":"crypto","text":"Does anybody know any good resources (e.g. books) to learn cryptogra' b':"post","channel":"crypto","text":"Does anybody know any good resources (e.g. books) to learn cryptogra'
b'phy? I\'m not speaking of basic concepts (e.g. what\'s a private key) but the actual mathematics behind' b"phy? I'm not speaking of basic concepts (e.g. what's a private key) but the actual mathematics behind"
b' the whole thing.\\nI have a copy of the \\"Handbook of Applied Cryptography\\" on my bookshelf but I f' b' the whole thing.\\nI have a copy of the \\"Handbook of Applied Cryptography\\" on my bookshelf but I f'
b'ound it too long/hard to follow. Are there any better alternatives?","mentions":[]},"signature":"hqKePb' b'ound it too long/hard to follow. Are there any better alternatives?","mentions":[]},"signature":"hqKePb'
b'bTXWxEi1njDnOWFsL0M0AoNoWyBFgNE6KXj//DThepaZSy9vRbygDHX5uNmCdyOrsQrwZsZhmUYKwtDQ==.sig.ed25519"}') b'bTXWxEi1njDnOWFsL0M0AoNoWyBFgNE6KXj//DThepaZSy9vRbygDHX5uNmCdyOrsQrwZsZhmUYKwtDQ==.sig.ed25519"}'
)
MSG_BODY_2 = (b'{"previous":"%iQRhPyqmNLpGaO1Tpm1I22jqnUEwRwkCTDbwAGtM+lY=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA' MSG_BODY_2 = (
b'{"previous":"%iQRhPyqmNLpGaO1Tpm1I22jqnUEwRwkCTDbwAGtM+lY=.sha256","author":"@1+Iwm79DKvVBqYKFkhT6fWRbA'
b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":103,"timestamp":1496674211806,"hash":"sha256","content":{"type"' b'VvNNVH4F2BSxwhYmx8=.ed25519","sequence":103,"timestamp":1496674211806,"hash":"sha256","content":{"type"'
b':"post","channel":"git-ssb","text":"Is it only me or `git.scuttlebot.io` is timing out?\\n\\nE.g. try a' b':"post","channel":"git-ssb","text":"Is it only me or `git.scuttlebot.io` is timing out?\\n\\nE.g. try a'
b'ccessing %vZCTqraoqKBKNZeATErXEtnoEr+wnT3p8tT+vL+29I4=.sha256","mentions":[{"link":"%vZCTqraoqKBKNZeATE' b'ccessing %vZCTqraoqKBKNZeATErXEtnoEr+wnT3p8tT+vL+29I4=.sha256","mentions":[{"link":"%vZCTqraoqKBKNZeATE'
b'rXEtnoEr+wnT3p8tT+vL+29I4=.sha256"}]},"signature":"+i4U0HUGDDEyNoNr2NIROPnT3WQj3RuTaIhY5koWW8f0vwr4tZsY' b'rXEtnoEr+wnT3p8tT+vL+29I4=.sha256"}]},"signature":"+i4U0HUGDDEyNoNr2NIROPnT3WQj3RuTaIhY5koWW8f0vwr4tZsY'
b'mAkqqMwFWfP+eBIbc7DZ835er6r6h9CwAg==.sig.ed25519"}') b'mAkqqMwFWfP+eBIbc7DZ835er6r6h9CwAg==.sig.ed25519"}'
)
class MockSHSSocket(SHSDuplexStream): class MockSHSSocket(SHSDuplexStream):
def __init__(self, *args, **kwargs): """A mocked SHS socket"""
super(MockSHSSocket, self).__init__()
self.input = [] def __init__(self): # pylint: disable=unused-argument
self.output = [] super().__init__()
self.is_connected = False
self._on_connect = [] self.input: List[bytes] = []
self.output: List[bytes] = []
self.is_connected = False
self._on_connect: List[Callable[[SHSDuplexStream], Awaitable[None]]] = []
def on_connect(self, cb: Callable[[SHSDuplexStream], Awaitable[None]]) -> None:
"""Set the on_connect callback"""
def on_connect(self, cb):
self._on_connect.append(cb) self._on_connect.append(cb)
async def read(self): async def read(self) -> bytes:
"""Read data from the socket"""
if not self.input: if not self.input:
raise StopAsyncIteration raise StopAsyncIteration
return self.input.pop(0) return self.input.pop(0)
def write(self, data): def write(self, data: bytes) -> None:
"""Write data to the socket"""
self.output.append(data) self.output.append(data)
def feed(self, input): def feed(self, input_: List[bytes]) -> None:
self.input += input """Feed data into the connection"""
self.input += input_
def get_output(self) -> Generator[bytes, None, None]:
"""Get the output of a call"""
def get_output(self):
while True: while True:
if not self.output: if not self.output:
break break
yield self.output.pop(0) yield self.output.pop(0)
def disconnect(self): def disconnect(self) -> None:
"""Disconnect from the remote party"""
self.is_connected = False self.is_connected = False
class MockSHSClient(MockSHSSocket): class MockSHSClient(MockSHSSocket):
async def connect(self): """A mocked SHS client"""
async def connect(self) -> None:
"""Connect to a SHS server"""
self.is_connected = True self.is_connected = True
for cb in self._on_connect: for cb in self._on_connect:
await cb() await cb(self)
class MockSHSServer(MockSHSSocket): class MockSHSServer(MockSHSSocket):
def listen(self): """A mocked SHS server"""
def listen(self) -> None:
"""Listen for new connections"""
self.is_connected = True self.is_connected = True
for cb in self._on_connect: for cb in self._on_connect:
ensure_future(cb()) ensure_future(cb(self))
@pytest.fixture @pytest.fixture
def ps_client(event_loop): def ps_client(event_loop: AbstractEventLoop) -> MockSHSClient: # pylint: disable=unused-argument
"""Fixture to provide a mocked SHS client"""
return MockSHSClient() return MockSHSClient()
@pytest.fixture @pytest.fixture
def ps_server(event_loop): def ps_server(event_loop: AbstractEventLoop) -> MockSHSServer: # pylint: disable=unused-argument
"""Fixture to provide a mocked SHS server"""
return MockSHSServer() return MockSHSServer()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_on_connect(ps_server): async def test_on_connect(ps_server: MockSHSServer) -> None: # pylint: disable=redefined-outer-name
"""Test the on_connect callback functionality"""
called = Event() called = Event()
async def _on_connect(): async def _on_connect(_: SHSDuplexStream) -> None:
called.set() called.set()
ps_server.on_connect(_on_connect) ps_server.on_connect(_on_connect)
@ -100,128 +144,132 @@ async def test_on_connect(ps_server):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_message_decoding(ps_client): async def test_message_decoding(ps_client: MockSHSClient) -> None: # pylint: disable=redefined-outer-name
"""Test message decoding"""
await ps_client.connect() await ps_client.connect()
ps = PacketStream(ps_client) ps = PacketStream(ps_client)
assert ps.is_connected assert ps.is_connected
ps_client.feed([ ps_client.feed(
b'\n\x00\x00\x00\x9a\x00\x00\x04\xfb', [
b"\n\x00\x00\x00\x9a\x00\x00\x04\xfb",
b'{"name":["createHistoryStream"],"args":[{"id":"@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519",' b'{"name":["createHistoryStream"],"args":[{"id":"@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519",'
b'"seq":10,"live":true,"keys":false}],"type":"source"}' b'"seq":10,"live":true,"keys":false}],"type":"source"}',
]) ]
)
messages = (await _collect_messages(ps)) messages = await _collect_messages(ps)
assert len(messages) == 1 assert len(messages) == 1
assert messages[0].type == PSMessageType.JSON assert messages[0].type == PSMessageType.JSON
assert messages[0].body == { assert messages[0].body == {
'name': ['createHistoryStream'], "name": ["createHistoryStream"],
'args': [ "args": [
{ {"id": "@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519", "seq": 10, "live": True, "keys": False}
'id': '@omgyp7Pnrw+Qm0I6T6Fh5VvnKmodMXwnxTIesW2DgMg=.ed25519',
'seq': 10,
'live': True,
'keys': False
}
], ],
'type': 'source' "type": "source",
} }
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_message_encoding(ps_client): async def test_message_encoding(ps_client: MockSHSClient) -> None: # pylint: disable=redefined-outer-name
"""Test message encoding"""
await ps_client.connect() await ps_client.connect()
ps = PacketStream(ps_client) ps = PacketStream(ps_client)
assert ps.is_connected assert ps.is_connected
ps.send({ ps.send(
'name': ['createHistoryStream'], {
'args': [{
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519",
'seq': 1,
'live': False,
'keys': False
}],
'type': 'source'
}, stream=True)
header, body = list(ps_client.get_output())
assert header == b'\x0a\x00\x00\x00\xa6\x00\x00\x00\x01'
assert json.loads(body.decode('utf-8')) == {
"name": ["createHistoryStream"], "name": ["createHistoryStream"],
"args": [ "args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False} {"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
], ],
"type": "source" "type": "source",
},
stream=True,
)
header, body = list(ps_client.get_output())
assert header == b"\x0a\x00\x00\x00\xa6\x00\x00\x00\x01"
assert json.loads(body.decode("utf-8")) == {
"name": ["createHistoryStream"],
"args": [
{"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
],
"type": "source",
} }
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_message_stream(ps_client, mocker): async def test_message_stream(ps_client: MockSHSClient, mocker: MockerFixture): # pylint: disable=redefined-outer-name
"""Test requesting a history stream"""
await ps_client.connect() await ps_client.connect()
ps = PacketStream(ps_client) ps = PacketStream(ps_client)
mocker.patch.object(ps, 'register_handler', wraps=ps.register_handler) mocker.patch.object(ps, "register_handler", wraps=ps.register_handler)
assert ps.is_connected assert ps.is_connected
ps.send({ ps.send(
'name': ['createHistoryStream'], {
'args': [{ "name": ["createHistoryStream"],
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "args": [
'seq': 1, {"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
'live': False, ],
'keys': False "type": "source",
}], },
'type': 'source' stream=True,
}, stream=True) )
assert ps.req_counter == 2 assert ps.req_counter == 2
assert ps.register_handler.call_count == 1 assert ps.register_handler.call_count == 1 # pylint: disable=no-member
handler = list(ps._event_map.values())[0][1] handler = list(ps._event_map.values())[0][1] # pylint: disable=protected-access
mock_process = mocker.AsyncMock()
mocker.patch.object(handler, 'process', mock_process) mock_process = mocker.patch.object(handler, "process")
ps_client.feed([b'\n\x00\x00\x02\xc5\xff\xff\xff\xff', MSG_BODY_1]) ps_client.feed([b"\n\x00\x00\x02\xc5\xff\xff\xff\xff", MSG_BODY_1])
msg = await ps.read() msg = await ps.read()
assert mock_process.await_count == 1 assert mock_process.await_count == 1
# responses have negative req # responses have negative req
assert msg.req == -1 assert msg.req == -1
assert msg.body['previous'] == '%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256' assert msg.body["previous"] == "%KTGP6W8vF80McRAZHYDWuKOD0KlNyKSq6Gb42iuV7Iw=.sha256"
assert ps.req_counter == 2 assert ps.req_counter == 2
stream_handler = ps.send({ stream_handler = ps.send(
'name': ['createHistoryStream'], {
'args': [{ "name": ["createHistoryStream"],
'id': "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "args": [
'seq': 1, {"id": "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519", "seq": 1, "live": False, "keys": False}
'live': False, ],
'keys': False "type": "source",
}], },
'type': 'source' stream=True,
}, stream=True) )
assert ps.req_counter == 3 assert ps.req_counter == 3
assert ps.register_handler.call_count == 2 assert ps.register_handler.call_count == 2 # pylint: disable=no-member
handler = list(ps._event_map.values())[1][1] handler = list(ps._event_map.values())[1][1] # pylint: disable=protected-access
mock_process = mocker.patch.object(handler, 'process', wraps=handler.process) mock_process = mocker.patch.object(handler, "process", wraps=handler.process)
ps_client.feed([b'\n\x00\x00\x02\xc5\xff\xff\xff\xfe', MSG_BODY_1,
b'\x0e\x00\x00\x023\xff\xff\xff\xfe', MSG_BODY_2]) ps_client.feed(
[b"\n\x00\x00\x02\xc5\xff\xff\xff\xfe", MSG_BODY_1, b"\x0e\x00\x00\x023\xff\xff\xff\xfe", MSG_BODY_2]
)
# execute both message polling and response handling loops # execute both message polling and response handling loops
collected, handled = await gather(_collect_messages(ps), _collect_messages(stream_handler)) collected, handled = await gather(_collect_messages(ps), _collect_messages(stream_handler))
# No messages collected, since they're all responses # No messages collected, since they're all responses
assert collected == [] assert collected == [None, None]
assert mock_process.call_count == 2 assert mock_process.call_count == 2
@ -231,34 +279,39 @@ async def test_message_stream(ps_client, mocker):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_message_request(ps_server, mocker): async def test_message_request(
ps_server: MockSHSServer, mocker: MockerFixture # pylint: disable=redefined-outer-name
) -> None:
"""Test message sending"""
ps_server.listen() ps_server.listen()
ps = PacketStream(ps_server) ps = PacketStream(ps_server)
mocker.patch.object(ps, 'register_handler', wraps=ps.register_handler) mocker.patch.object(ps, "register_handler", wraps=ps.register_handler)
ps.send({ ps.send({"name": ["whoami"], "args": []})
'name': ['whoami'],
'args': []
})
header, body = list(ps_server.get_output()) header, body = list(ps_server.get_output())
assert header == b'\x02\x00\x00\x00 \x00\x00\x00\x01' assert header == b"\x02\x00\x00\x00 \x00\x00\x00\x01"
assert json.loads(body.decode('utf-8')) == {"name": ["whoami"], "args": []} assert json.loads(body.decode("utf-8")) == {"name": ["whoami"], "args": []}
assert ps.req_counter == 2 assert ps.req_counter == 2
assert ps.register_handler.call_count == 1 assert ps.register_handler.call_count == 1 # pylint: disable=no-member
handler = list(ps._event_map.values())[0][1] handler = list(ps._event_map.values())[0][1] # pylint: disable=protected-access
mock_process = mocker.AsyncMock()
mocker.patch.object(handler, 'process', mock_process) mock_process = mocker.patch.object(handler, "process")
ps_server.feed([b'\x02\x00\x00\x00>\xff\xff\xff\xff',
b'{"id":"@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519"}']) ps_server.feed(
[
b"\x02\x00\x00\x00>\xff\xff\xff\xff",
b'{"id":"@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519"}',
]
)
msg = await ps.read() msg = await ps.read()
assert mock_process.await_count == 1 assert mock_process.await_count == 1
# responses have negative req # responses have negative req
assert msg.req == -1 assert msg.req == -1
assert msg.body['id'] == '@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519' assert msg.body["id"] == "@1+Iwm79DKvVBqYKFkhT6fWRbAVvNNVH4F2BSxwhYmx8=.ed25519"
assert ps.req_counter == 2 assert ps.req_counter == 2

View File

@ -1,10 +1,11 @@
"""Test for utility functions"""
from base64 import b64decode from base64 import b64decode
from unittest.mock import mock_open, patch from unittest.mock import mock_open, patch
import pytest import pytest
from ssb.util import load_ssb_secret, ConfigException from ssb.util import ConfigException, load_ssb_secret
CONFIG_FILE = """ CONFIG_FILE = """
## Comments should be supported too ## Comments should be supported too
@ -16,21 +17,25 @@ CONFIG_FILE = """
} }
""" """
CONFIG_FILE_INVALID = CONFIG_FILE.replace('ed25519', 'foo') CONFIG_FILE_INVALID = CONFIG_FILE.replace("ed25519", "foo")
def test_load_secret(): def test_load_secret() -> None:
with patch('ssb.util.open', mock_open(read_data=CONFIG_FILE), create=True): """Test loading the SSB secret from a file"""
with patch("ssb.util.open", mock_open(read_data=CONFIG_FILE), create=True):
secret = load_ssb_secret() secret = load_ssb_secret()
priv_key = b'\xfd\xba\x83\x04\x8f\xef\x18\xb0\xf9\xab-\xc6\xc4\xcb \x1cX\x18"\xba\xd8\xd3\xc2_O5\x1a\t\x84\xfa\xc7A' priv_key = b'\xfd\xba\x83\x04\x8f\xef\x18\xb0\xf9\xab-\xc6\xc4\xcb \x1cX\x18"\xba\xd8\xd3\xc2_O5\x1a\t\x84\xfa\xc7A'
assert secret['id'] == '@rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=.ed25519' assert secret["id"] == "@rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=.ed25519"
assert bytes(secret['keypair']) == priv_key assert bytes(secret["keypair"]) == priv_key
assert bytes(secret['keypair'].verify_key) == b64decode('rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=') assert bytes(secret["keypair"].verify_key) == b64decode("rsYpBIcXsxjQAf0JNes+MHqT2DL+EfopWKAp4rGeEPQ=")
def test_load_exception(): def test_load_exception() -> None:
"""Test configuration loading if there is a problem with the file"""
with pytest.raises(ConfigException): with pytest.raises(ConfigException):
with patch('ssb.util.open', mock_open(read_data=CONFIG_FILE_INVALID), create=True): with patch("ssb.util.open", mock_open(read_data=CONFIG_FILE_INVALID), create=True):
load_ssb_secret() load_ssb_secret()