diff --git a/Makefile b/Makefile index a33e0c30e..aa77e979a 100644 --- a/Makefile +++ b/Makefile @@ -105,7 +105,7 @@ build-self: ensure-docker cd libs/gl-client-py; maturin develop #mypy examples/python -check-all: check-rs check-self check-py +check-all: check-rs check-self check-py check-testing-py check-self: ensure-docker build-self PYTHONPATH=/repo/libs/gl-testing \ diff --git a/libs/gl-testing/Makefile b/libs/gl-testing/Makefile index 7af435a79..1f6b6d5d5 100644 --- a/libs/gl-testing/Makefile +++ b/libs/gl-testing/Makefile @@ -18,10 +18,15 @@ GENALL += ${GEN_TESTING} ${TESTINGDIR}/gltesting/scheduler_grpc.py: testgrpc +check-testing-py: + #cd ${TESTINGDIR}; mypy gltesting + cd ${TESTINGDIR}; poetry install --with=dev; poetry run pytest tests -n 4 -vvv + testgrpc: ${REPO_ROOT}/libs/proto/glclient/scheduler.proto python -m grpc_tools.protoc ${TESTPROTOC_OPTS} glclient/scheduler.proto mv ${TESTINGDIR}/gltesting/glclient/scheduler_grpc.py ${TESTINGDIR}/gltesting/scheduler_grpc.py rm -rf ${TESTINGDIR}/gltesting/glclient - +protoc: + uv run python3 -m grpc_tools.protoc -I. --python_out=. --pyi_out=. --purerpc_out=. --grpc_python_out=. gltesting/test.proto diff --git a/libs/gl-testing/gltesting/fixtures.py b/libs/gl-testing/gltesting/fixtures.py index 913439008..11ee3bbdf 100644 --- a/libs/gl-testing/gltesting/fixtures.py +++ b/libs/gl-testing/gltesting/fixtures.py @@ -10,10 +10,22 @@ from pathlib import Path import logging import sys -from pyln.testing.fixtures import bitcoind, teardown_checks, node_cls, test_name, executor, db_provider, test_base_dir, jsonschemas +from pyln.testing.fixtures import ( + bitcoind, + teardown_checks, + node_cls, + test_name, + executor, + db_provider, + test_base_dir, + jsonschemas, +) from gltesting.network import node_factory from pyln.testing.fixtures import directory as str_directory from decimal import Decimal +from gltesting.grpcweb import GrpcWebProxy, NodeHandler +from clnvm import ClnVersionManager + logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) logging.getLogger().addHandler(logging.StreamHandler(sys.stdout)) @@ -21,10 +33,35 @@ logging.getLogger("hpack").setLevel(logging.ERROR) logger = logging.getLogger(__name__) + +@pytest.fixture(autouse=True) +def paths(): + """A fixture to ensure that we have all CLN versions and that + `PATH` points to the latest one. + + If you'd like to test a development version rather than the + released ones, ensure that its executable path is in the PATH + before calling `pytest` since this appends to the end. + + """ + vm = ClnVersionManager() + versions = vm.get_versions() + + # Should be a no-op after the first run + vm.get_all() + + latest = [v for v in versions if "gl" in v.tag][-1] + + os.environ["PATH"] += f":{vm.get_target_path(latest) / 'usr' / 'local' / 'bin'}" + + yield + + @pytest.fixture() -def directory(str_directory : str) -> Path: +def directory(str_directory: str) -> Path: return Path(str_directory) / "gl-testing" + @pytest.fixture() def cert_directory(directory): yield directory / "certs" @@ -79,31 +116,33 @@ def scheduler(scheduler_id, bitcoind): btcproxy = bitcoind.get_proxy() # Copied from pyln.testing.utils.NodeFactory.get_node - feerates=(15000, 11000, 7500, 3750) + feerates = (15000, 11000, 7500, 3750) def mock_estimatesmartfee(r): - params = r['params'] - if params == [2, 'CONSERVATIVE']: + params = r["params"] + if params == [2, "CONSERVATIVE"]: feerate = feerates[0] * 4 - elif params == [6, 'ECONOMICAL']: + elif params == [6, "ECONOMICAL"]: feerate = feerates[1] * 4 - elif params == [12, 'ECONOMICAL']: + elif params == [12, "ECONOMICAL"]: feerate = feerates[2] * 4 - elif params == [100, 'ECONOMICAL']: + elif params == [100, "ECONOMICAL"]: feerate = feerates[3] * 4 else: - warnings.warn("Don't have a feerate set for {}/{}.".format( - params[0], params[1], - )) + warnings.warn( + "Don't have a feerate set for {}/{}.".format( + params[0], + params[1], + ) + ) feerate = 42 return { - 'id': r['id'], - 'error': None, - 'result': { - 'feerate': Decimal(feerate) / 10**8 - }, + "id": r["id"], + "error": None, + "result": {"feerate": Decimal(feerate) / 10**8}, } - btcproxy.mock_rpc('estimatesmartfee', mock_estimatesmartfee) + + btcproxy.mock_rpc("estimatesmartfee", mock_estimatesmartfee) s = Scheduler(bitcoind=btcproxy, grpc_port=grpc_port, identity=scheduler_id) logger.debug(f"Scheduler is running at {s.grpc_addr}") @@ -123,9 +162,7 @@ def mock_estimatesmartfee(r): # here. if s.debugger.reports != []: - raise ValueError( - f"Some signer reported an error: {s.debugger.reports}" - ) + raise ValueError(f"Some signer reported an error: {s.debugger.reports}") @pytest.fixture() @@ -134,3 +171,65 @@ def clients(directory, scheduler, nobody_id): directory=directory / "clients", scheduler=scheduler, nobody_id=nobody_id ) yield clients + + +@pytest.fixture(scope="session", autouse=True) +def cln_path() -> Path: + """Ensure that the latest CLN version is in PATH. + + Some tests use the NodeFactory directly, which relies on being + able to pick a recent CLN version from the `$PATH`. By appending + at the end we just ensure that there is a CLN version to be found. + + This is our Elephant in Cairo :-) + https://en.wikipedia.org/wiki/Elephant_in_Cairo + """ + manager = ClnVersionManager() + v = manager.latest() + os.environ["PATH"] += f":{v.bin_path}" + return v.bin_path + + +@pytest.fixture() +def grpc_test_server(): + """Creates a hello world server over grpc to test the web proxy against. + + We explicitly do not use the real protos since the proxy must be + agnostic. + + """ + import anyio + from threading import Thread + import purerpc + from util.grpcserver import Server + + server = Server() + logging.getLogger("purerpc").setLevel(logging.DEBUG) + server.start() + + yield server + + server.stop() + + +@pytest.fixture() +def grpc_web_proxy(scheduler, grpc_test_server): + p = GrpcWebProxy(scheduler=scheduler, grpc_port=grpc_test_server.grpc_port) + p.start() + + yield p + + p.stop() + + +@pytest.fixture +def node_grpc_web_proxy(scheduler): + """A grpc-web proxy that knows how to talk to nodes. + """ + p = GrpcWebProxy(scheduler=scheduler, grpc_port=0) + p.handler_cls = NodeHandler + p.start() + + yield p + + p.stop() diff --git a/libs/gl-testing/gltesting/grpcweb.py b/libs/gl-testing/gltesting/grpcweb.py new file mode 100644 index 000000000..05a668dff --- /dev/null +++ b/libs/gl-testing/gltesting/grpcweb.py @@ -0,0 +1,210 @@ +# A simple grpc-web proxy enabling web-clients to talk to +# Greenlight. Unlike the direct grpc interface exposed by the node and +# the node domain proxy, the grpc-web proxy does not require a client +# certificate from the client, making it possible for browsers to talk +# to it. The client authentication via client certificates is no +# longer present, but the payloads are still signed by the authorized +# client, assuring authentication of the client. + +from gltesting.scheduler import Scheduler +from ephemeral_port_reserve import reserve +from threading import Thread, Event +from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler +import logging +import struct +import httpx +from dataclasses import dataclass +from typing import Dict +import ssl + + +class GrpcWebProxy(object): + def __init__(self, scheduler: Scheduler, grpc_port: int): + self.logger = logging.getLogger("gltesting.grpcweb.GrpcWebProxy") + self.scheduler = scheduler + self.web_port = reserve() + self._thread: None | Thread = None + self.running = False + self.grpc_port = grpc_port + self.httpd: None | ThreadingHTTPServer = None + self.logger.info( + f"GrpcWebProxy configured to forward requests from web_port={self.web_port} to grpc_port={self.grpc_port}" + ) + self.handler_cls = Handler + + def start(self): + self._thread = Thread(target=self.run, daemon=True) + self.logger.info(f"Starting grpc-web-proxy on port {self.web_port}") + self.running = True + server_address = ("127.0.0.1", self.web_port) + + self.httpd = ThreadingHTTPServer(server_address, self.handler_cls) + self.httpd.grpc_port = self.grpc_port + + # Just a simple way to pass the scheduler to the handler + self.httpd.scheduler = self.scheduler + + self.logger.debug(f"Server startup complete") + self._thread.start() + + def run(self): + self.httpd.serve_forever() + + def stop(self): + self.logger.info(f"Stopping grpc-web-proxy running on port {self.web_port}") + self.httpd.shutdown() + self._thread.join() + + +@dataclass +class Request: + body: bytes + headers: Dict[str, str] + flags: int + length: int + + +@dataclass +class Response: + body: bytes + + +class Handler(BaseHTTPRequestHandler): + def __init__(self, *args, **kwargs): + self.logger = logging.getLogger("gltesting.grpcweb.Handler") + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + + def proxy(self, request) -> Response: + """Callback called with the request, implementing the proxying.""" + url = f"http://localhost:{self.server.grpc_port}{self.path}" + self.logger.debug(f"Forwarding request to '{url}'") + headers = { + "te": "trailers", + "Content-Type": "application/grpc", + "grpc-accept-encoding": "identity", + "user-agent": "gl-testing-grpc-web-proxy", + } + content = struct.pack("!cI", request.flags, request.length) + request.body + req = httpx.Request( + "POST", + url, + headers=headers, + content=content, + ) + client = httpx.Client(http1=False, http2=True) + res = client.send(req) + return Response(body=res.content) + + def auth(self, request: Request) -> bool: + """Authenticate the request. True means allow.""" + return True + + def do_POST(self): + # We don't actually touch the payload, so we do not really + # care about the flags ourselves. The upstream sysmte will + # though. + flags = self.rfile.read(1) + + # We have the length from above already, but that includes the + # header. Ensure that the two values match up. + strlen = self.rfile.read(4) + (length,) = struct.unpack_from("!I", strlen) + l = int(self.headers.get("Content-Length")) + assert l == length + 5 + + # Now we can finally read the body, It is kept as is, so no + # need to decode it, and we can treat it as opaque blob. + body = self.rfile.read(length) + + req = Request(body=body, headers=self.headers, flags=flags, length=length) + if not self.auth(req): + self.wfile.write(b"HTTP/1.1 401 Unauthorized\r\n\r\n") + return + + response = self.proxy(req) + self.wfile.write(b"HTTP/1.0 200 OK\n\n") + self.wfile.write(response.body) + self.wfile.flush() + + +class NodeHandler(Handler): + """A handler that is aware of nodes, their auth and how they schedule.""" + + def __init__(self, *args, **kwargs): + self.logger = logging.getLogger("gltesting.grpcweb.NodeHandler") + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + + def auth(self, request: Request) -> bool: + # TODO extract the `glauthpubkey` and the `glauthsig`, then + # verify them. Fail the call if the verification fails, + # forward otherwise. + # This is just a test server, and we don't make use of the + # multiplexing support in `h2`, which simplifies this proxy + # quite a bit. The production server maintains a cache of + # connections and multiplexes correctly. + pk = request.headers.get("glauthpubkey", None) + sig = request.headers.get("glauthsig", None) + ts = request.headers.get("glts", None) + + if not pk: + self.logger.warn(f"Missing public key header") + return False + + if not sig: + self.logger.warn(f"Missing signature header") + return False + + if not ts: + self.logger.warn(f"Missing timestamp header") + return False + + # TODO Check the signature. + return True + + def proxy(self, request: Request): + # Fetch current location of the node + + pk = request.headers.get("glauthpubkey") + from base64 import b64decode + + pk = b64decode(pk) + + node = self.server.scheduler.get_node(pk) + self.logger.debug(f"Found node for node_id={pk.hex()}") + + # TODO Schedule node if not scheduled + + client_cert = node.identity.private_key + ca_path = node.identity.caroot_path + + # Load TLS client cert info client + ctx = httpx.create_ssl_context( + verify=ca_path, + http2=True, + cert=( + node.identity.cert_chain_path, + node.identity.private_key_path, + ), + ) + client = httpx.Client(http1=False, http2=True, verify=ctx) + + url = f"{node.process.grpc_uri}{self.path}" + headers = { + "te": "trailers", + "Content-Type": "application/grpc", + "grpc-accept-encoding": "identity", + "user-agent": "gl-testing-grpc-web-proxy", + } + content = struct.pack("!cI", request.flags, request.length) + request.body + + # Forward request + req = httpx.Request( + "POST", + url, + headers=headers, + content=content, + ) + res = client.send(req) + + # Return response + return Response(body=res.content) diff --git a/libs/gl-testing/gltesting/test.proto b/libs/gl-testing/gltesting/test.proto new file mode 100644 index 000000000..c2f00bc86 --- /dev/null +++ b/libs/gl-testing/gltesting/test.proto @@ -0,0 +1,17 @@ +// Just a small grpc definition to test the grpcweb implementation. + +syntax = "proto3"; + +package gltesting; + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/libs/gl-testing/gltesting/test_grpc.py b/libs/gl-testing/gltesting/test_grpc.py new file mode 100644 index 000000000..e2b09c3df --- /dev/null +++ b/libs/gl-testing/gltesting/test_grpc.py @@ -0,0 +1,39 @@ +import purerpc +import gltesting.test_pb2 as gltesting_dot_test__pb2 + + +class GreeterServicer(purerpc.Servicer): + async def SayHello(self, input_message): + raise NotImplementedError() + + @property + def service(self) -> purerpc.Service: + service_obj = purerpc.Service( + "gltesting.Greeter" + ) + service_obj.add_method( + "SayHello", + self.SayHello, + purerpc.RPCSignature( + purerpc.Cardinality.UNARY_UNARY, + gltesting_dot_test__pb2.HelloRequest, + gltesting_dot_test__pb2.HelloReply, + ) + ) + return service_obj + + +class GreeterStub: + def __init__(self, channel): + self._client = purerpc.Client( + "gltesting.Greeter", + channel + ) + self.SayHello = self._client.get_method_stub( + "SayHello", + purerpc.RPCSignature( + purerpc.Cardinality.UNARY_UNARY, + gltesting_dot_test__pb2.HelloRequest, + gltesting_dot_test__pb2.HelloReply, + ) + ) \ No newline at end of file diff --git a/libs/gl-testing/gltesting/test_pb2.py b/libs/gl-testing/gltesting/test_pb2.py new file mode 100644 index 000000000..efa688191 --- /dev/null +++ b/libs/gl-testing/gltesting/test_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: gltesting/test.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14gltesting/test.proto\x12\tgltesting\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2E\n\x07Greeter\x12:\n\x08SayHello\x12\x17.gltesting.HelloRequest\x1a\x15.gltesting.HelloReplyb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gltesting.test_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_HELLOREQUEST']._serialized_start=35 + _globals['_HELLOREQUEST']._serialized_end=63 + _globals['_HELLOREPLY']._serialized_start=65 + _globals['_HELLOREPLY']._serialized_end=94 + _globals['_GREETER']._serialized_start=96 + _globals['_GREETER']._serialized_end=165 +# @@protoc_insertion_point(module_scope) diff --git a/libs/gl-testing/gltesting/test_pb2.pyi b/libs/gl-testing/gltesting/test_pb2.pyi new file mode 100644 index 000000000..bf0bd395a --- /dev/null +++ b/libs/gl-testing/gltesting/test_pb2.pyi @@ -0,0 +1,17 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class HelloRequest(_message.Message): + __slots__ = ("name",) + NAME_FIELD_NUMBER: _ClassVar[int] + name: str + def __init__(self, name: _Optional[str] = ...) -> None: ... + +class HelloReply(_message.Message): + __slots__ = ("message",) + MESSAGE_FIELD_NUMBER: _ClassVar[int] + message: str + def __init__(self, message: _Optional[str] = ...) -> None: ... diff --git a/libs/gl-testing/gltesting/test_pb2_grpc.py b/libs/gl-testing/gltesting/test_pb2_grpc.py new file mode 100644 index 000000000..395457cd5 --- /dev/null +++ b/libs/gl-testing/gltesting/test_pb2_grpc.py @@ -0,0 +1,66 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from gltesting import test_pb2 as gltesting_dot_test__pb2 + + +class GreeterStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.SayHello = channel.unary_unary( + '/gltesting.Greeter/SayHello', + request_serializer=gltesting_dot_test__pb2.HelloRequest.SerializeToString, + response_deserializer=gltesting_dot_test__pb2.HelloReply.FromString, + ) + + +class GreeterServicer(object): + """Missing associated documentation comment in .proto file.""" + + def SayHello(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GreeterServicer_to_server(servicer, server): + rpc_method_handlers = { + 'SayHello': grpc.unary_unary_rpc_method_handler( + servicer.SayHello, + request_deserializer=gltesting_dot_test__pb2.HelloRequest.FromString, + response_serializer=gltesting_dot_test__pb2.HelloReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'gltesting.Greeter', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Greeter(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def SayHello(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gltesting.Greeter/SayHello', + gltesting_dot_test__pb2.HelloRequest.SerializeToString, + gltesting_dot_test__pb2.HelloReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/libs/gl-testing/pyproject.toml b/libs/gl-testing/pyproject.toml index 45f4a5304..b0df7cc16 100644 --- a/libs/gl-testing/pyproject.toml +++ b/libs/gl-testing/pyproject.toml @@ -1,51 +1,30 @@ -[tool.poetry] +[project] name = "gltesting" version = "0.3.0" description = "" -authors = ["Christian Decker "] -license = "MIT" - -packages = [ - { include = "gltesting" }, +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "cln-version-manager", + "flaky>=3.8.1", + "gl-client", + "grpcio-tools>=1.66", + "grpcio>=1.66.0", + "httpx>=0.27.2", + "purerpc>=0.8.0", + "pyln-client==24.2", + "pyln-testing==24.2", + "pytest-timeout>=2.3.1", + "pytest-xdist>=3.6.1", + "rich>=13.9.3", + "sh>=1.14.3", + "sonora>=0.2.3", ] -[tool.poetry.dependencies] -python = "^3.8" -cryptography = ">=36.0.1" -ephemeral-port-reserve = "^1.1.4" -sh = "^1.14.2" -pytest-timeout = "^2.1.0" -pytest-xdist = "^2.5.0" -pytest = "^7.1.2" -pytest-cov = "^3.0.0" -rich = "^12.5.1" -pyln-testing = "==24.02" -pyln-client = "==24.02" -pyln-grpc-proto = "^0.1" -protobuf = ">=3" -flaky = "^3" -anyio = "^3.7.1" -aiostream = "^0.5.1" -purerpc = { version = "^0.8.0", extras = ["grpc"] } -pytest-sugar = "^0.9.7" - -# The dependency below is defined in this repository -# PyPi cannot support relative paths. -# -# Users, who `pip install` this library retrieve the sources from `PyPi`. -# using the version numbers listed below -# -# Contributors should use `poetry install` in the project root -# to insure the installation is editable from local sources. -gl-client = "0.3.0" -cln-version-manager = "^0.1.0" -[tool.poetry.group.dev.dependencies] -mypy = "^1" -typed-ast = "^1.5.4" -cln-version-manager = { path="../cln-version-manager", develop=true } -grpcio-tools = "^1.62.1" +[tool.uv] +package = true -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" +[tool.uv.sources] +gl-client = { path = "../gl-client-py" } +cln-version-manager = { path = "../cln-version-manager" } diff --git a/libs/gl-testing/tests/test_grpc_web.py b/libs/gl-testing/tests/test_grpc_web.py new file mode 100644 index 000000000..be37a3550 --- /dev/null +++ b/libs/gl-testing/tests/test_grpc_web.py @@ -0,0 +1,64 @@ +# Tests that use a grpc-web client, without a client certificate, but +# payload signing for authentication. + +from gltesting.fixtures import * +from gltesting.test_pb2_grpc import GreeterStub +from gltesting.test_pb2 import HelloRequest +import sonora.client +from pyln import grpc as clnpb +from base64 import b64encode +from time import time +import struct +from typing import Any + + +class GrpcWebClient: + """A simple grpc-web client that implements the calling convention.""" + + def __init__(self, node_grpc_web_proxy_uri, node_id: bytes): + self.node_id = node_id + self.node_grpc_web_proxy_uri = node_grpc_web_proxy_uri + self.channel = sonora.client.insecure_web_channel(node_grpc_web_proxy_uri) + self.stub = clnpb.NodeStub(self.channel) + + def call(self, method_name: str, req: Any) -> Any: + ts = struct.pack("!Q", int(time() * 1000)) + metadata = [ + ("glauthpubkey", b64encode(self.node_id).decode("ASCII")), + ("glauthsig", b64encode(b"\x00" * 64).decode("ASCII")), + ("glts", b64encode(ts).decode("ASCII")), + ] + func = self.stub.__dict__.get(method_name) + return func(req, metadata=metadata) + + +def test_start(grpc_web_proxy): + with sonora.client.insecure_web_channel( + f"http://localhost:{grpc_web_proxy.web_port}" + ) as channel: + stub = GreeterStub(channel) + req = HelloRequest(name="greenlight") + print(stub.SayHello(req)) + + +def test_node_grpc_web(scheduler, node_grpc_web_proxy, clients): + """Ensure that the""" + # Start by creating a node + c = clients.new() + c.register(configure=True) + n = c.node() + info = n.get_info() + + # Now extract the TLS certificates, so we can sign the payload. + # TODO Configure the web client to sign its requests too + node_id = info.id + key_path = c.directory / "device-key.pem" + ca_path = c.directory / "ca.pem" + + proxy_uri = f"http://localhost:{node_grpc_web_proxy.web_port}" + web_client = GrpcWebClient(proxy_uri, node_id) + + # Issue a request to the node through the proxy. + req = clnpb.GetinfoRequest() + info = web_client.call("Getinfo", req) + print(info) diff --git a/libs/gl-testing/tests/util/grpcserver.py b/libs/gl-testing/tests/util/grpcserver.py new file mode 100644 index 000000000..34fbc13fe --- /dev/null +++ b/libs/gl-testing/tests/util/grpcserver.py @@ -0,0 +1,36 @@ +# This is a simple grpc server serving the `gltesting/test.proto` +# protocol. It is used to test whether the grpc-web to grpc/h2 +# proxying is working. + +from gltesting.test_pb2 import HelloRequest, HelloReply +from gltesting.test_grpc import GreeterServicer +from ephemeral_port_reserve import reserve +import purerpc +from threading import Thread +import anyio + + + +class Server(GreeterServicer): + def __init__(self, *args, **kwargs): + GreeterServicer.__init__(self, *args, **kwargs) + self.grpc_port = reserve() + self.inner = purerpc.Server(self.grpc_port) + self.thread: Thread | None = None + self.inner.add_service(self.service) + + async def SayHello(self, message): + return HelloReply(message="Hello, " + message.name) + + def start(self): + def target(): + try: + anyio.run(self.inner.serve_async) + except Exception as e: + print("Error starting the grpc backend") + + self.thread = Thread(target=target, daemon=True) + self.thread.start() + + def stop(self): + pass diff --git a/pyproject.toml b/pyproject.toml index 8c47ee4e5..76029b65b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,3 +39,5 @@ pillow = "^9.5.0" [tool.poetry.group.lsp_ide.dependencies] python-lsp-server = "^1.10.0" +[tool.uv.workspace] +members = ["libs/gl-testing"]