Skip to content

Commit ea063d1

Browse files
committed
testing: Add a simple grpc-web proxy
This proxy is used in the local testing environment to provide node-access to browser based clients. It strips the transport authentication, and replaces it with the payload authentication already used for the signer context.
1 parent 375337f commit ea063d1

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# A simple grpc-web proxy enabling web-clients to talk to
2+
# Greenlight. Unlike the direct grpc interface exposed by the node and
3+
# the node domain proxy, the grpc-web proxy does not require a client
4+
# certificate from the client, making it possible for browsers to talk
5+
# to it. The client authentication via client certificates is no
6+
# longer present, but the payloads are still signed by the authorized
7+
# client, assuring authentication of the client.
8+
9+
from gltesting.scheduler import Scheduler
10+
from ephemeral_port_reserve import reserve
11+
from threading import Thread, Event
12+
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
13+
import logging
14+
import struct
15+
16+
17+
class GrpcWebProxy(object):
18+
def __init__(self, scheduler: Scheduler, grpc_port: int):
19+
self.logger = logging.getLogger("gltesting.grpcweb.GrpcWebProxy")
20+
self.scheduler = scheduler
21+
self.web_port = reserve()
22+
self._thread: None | Thread = None
23+
self.running = False
24+
self.grpc_port = grpc_port
25+
self.httpd: None | ThreadingHTTPServer = None
26+
self.logger.info(
27+
f"GrpcWebProxy configured to forward requests from web_port={self.web_port} to grpc_port={self.grpc_port}"
28+
)
29+
30+
def start(self):
31+
self._thread = Thread(target=self.run, daemon=True)
32+
self.logger.info(f"Starting grpc-web-proxy on port {self.web_port}")
33+
self.running = True
34+
server_address = ("127.0.0.1", self.web_port)
35+
36+
self.httpd = ThreadingHTTPServer(server_address, Handler)
37+
self.httpd.grpc_port = self.grpc_port
38+
self.logger.debug(f"Server startup complete")
39+
self._thread.start()
40+
41+
def run(self):
42+
self.httpd.serve_forever()
43+
44+
def stop(self):
45+
self.logger.info(f"Stopping grpc-web-proxy running on port {self.web_port}")
46+
self.httpd.shutdown()
47+
self._thread.join()
48+
49+
50+
class Handler(BaseHTTPRequestHandler):
51+
def __init__(self, *args, **kwargs):
52+
self.logger = logging.getLogger("gltesting.grpcweb.Handler")
53+
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
54+
55+
def do_POST(self):
56+
# We don't actually touch the payload, so we do not really
57+
# care about the flags ourselves. The upstream sysmte will
58+
# though.
59+
flags = self.rfile.read(1)
60+
61+
# We have the length from above already, but that includes the
62+
# header. Ensure that the two values match up.
63+
strlen = self.rfile.read(4)
64+
(length,) = struct.unpack_from("!I", strlen)
65+
l = int(self.headers.get("Content-Length"))
66+
assert l == length + 5
67+
68+
# Now we can finally read the body, It is kept as is, so no
69+
# need to decode it, and we can treat it as opaque blob.
70+
body = self.rfile.read(length)
71+
72+
# TODO extract the `glauthpubkey` and the `glauthsig`, then
73+
# verify them. Fail the call if the verification fails,
74+
# forward otherwise.
75+
# This is just a test server, and we don't make use of the
76+
# multiplexing support in `h2`, which simplifies this proxy
77+
# quite a bit. The production server maintains a cache of
78+
# connections and multiplexes correctly.
79+
80+
import httpx
81+
82+
url = f"http://localhost:{self.server.grpc_port}{self.path}"
83+
self.logger.debug(f"Forwarding request to '{url}'")
84+
headers = {
85+
"te": "trailers",
86+
"Content-Type": "application/grpc",
87+
"grpc-accept-encoding": "idenity",
88+
"user-agent": "My bloody hacked up script",
89+
}
90+
content = struct.pack("!cI", flags, length) + body
91+
req = httpx.Request(
92+
"POST",
93+
url,
94+
headers=headers,
95+
content=content,
96+
)
97+
client = httpx.Client(http1=False, http2=True)
98+
99+
res = client.send(req)
100+
res = client.send(req)
101+
102+
canned = b"\n\rheklllo world"
103+
l = struct.pack("!I", len(canned))
104+
self.wfile.write(b"HTTP/1.0 200 OK\n\n")
105+
self.wfile.write(b"\x00")
106+
self.wfile.write(l)
107+
self.wfile.write(canned)
108+
self.wfile.flush()

0 commit comments

Comments
 (0)