Use Starlette, uvicorn and websockets; rename Docker files to container ones
Serve neo4j-browser with Starlette and uvicorn (the optional browser extra) instead of a hand-written HTTP server, and handle Bolt over WebSocket with the websockets library's Sans-I/O protocol. Rename Dockerfile, .dockerignore and docker/ to Containerfile, .containerignore and container/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e8e498b56
commit
f0fb9ddc74
18 files changed
+189
-162
No files matched your search
+48
-50
@@ -7,16 +7,18 @@ by neo4j-browser as well as current Python/Java drivers and cypher-shell.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import itertools
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from websockets.frames import CloseCode, Opcode
|
||||
from websockets.protocol import State
|
||||
from websockets.server import ServerProtocol
|
||||
|
||||
from . import packstream
|
||||
from .backend import BoltFailure, Config, Session
|
||||
from .discovery import discovery_document
|
||||
from .packstream import Structure
|
||||
from .web import discovery_document
|
||||
|
||||
log = logging.getLogger("peo.bolt")
|
||||
|
||||
@@ -61,11 +63,14 @@ class RawTransport:
|
||||
|
||||
|
||||
class WebSocketTransport(RawTransport):
|
||||
"""Minimal RFC 6455 server: Bolt bytes are carried in binary frames."""
|
||||
"""Bolt over WebSocket: Bolt bytes are carried in binary frames.
|
||||
|
||||
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
Uses the Sans-I/O protocol of the websockets library, as this port also speaks raw Bolt.
|
||||
"""
|
||||
|
||||
async def handshake(self):
|
||||
# Read the request head here rather than in websockets, which only accepts GET:
|
||||
# neo4j-browser also sends plain HTTP requests (discovery, CORS preflight) to this port.
|
||||
request = bytearray(self.buffer)
|
||||
self.buffer.clear()
|
||||
while b"\r\n\r\n" not in request:
|
||||
@@ -73,21 +78,26 @@ class WebSocketTransport(RawTransport):
|
||||
if not chunk or len(request) > 65536:
|
||||
raise ConnectionClosed()
|
||||
request += chunk
|
||||
head, _, rest = bytes(request).partition(b"\r\n\r\n")
|
||||
head = bytes(request).partition(b"\r\n\r\n")[0]
|
||||
headers = {}
|
||||
for line in head.decode("latin-1").split("\r\n")[1:]:
|
||||
k, _, v = line.partition(":")
|
||||
headers[k.strip().lower()] = v.strip()
|
||||
method = head.split(b" ", 1)[0]
|
||||
key = headers.get("sec-websocket-key")
|
||||
if method != b"GET" or not key:
|
||||
if method != b"GET" or "websocket" not in headers.get("upgrade", "").lower():
|
||||
await self._plain_http(method, headers)
|
||||
raise ConnectionClosed()
|
||||
accept = base64.b64encode(hashlib.sha1((key + self.GUID).encode()).digest()).decode()
|
||||
self.writer.write(("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\nSec-WebSocket-Accept: {accept}\r\n\r\n").encode())
|
||||
await self.writer.drain()
|
||||
self.raw = bytearray(rest)
|
||||
|
||||
self.protocol = ServerProtocol(max_size=None)
|
||||
self.protocol.receive_data(bytes(request))
|
||||
events = self.protocol.events_received()
|
||||
if not events:
|
||||
raise ConnectionClosed()
|
||||
response = self.protocol.accept(events[0])
|
||||
self.protocol.send_response(response)
|
||||
await self._flush()
|
||||
if response.status_code != 101:
|
||||
raise ConnectionClosed()
|
||||
|
||||
async def _plain_http(self, method, headers):
|
||||
"""Answer non-WebSocket requests, e.g. neo4j-browser probing for discovery data."""
|
||||
@@ -104,52 +114,39 @@ class WebSocketTransport(RawTransport):
|
||||
self.writer.write(response)
|
||||
await self.writer.drain()
|
||||
|
||||
async def _raw(self, n):
|
||||
while len(self.raw) < n:
|
||||
chunk = await self.reader.read(65536)
|
||||
if not chunk:
|
||||
raise ConnectionClosed()
|
||||
self.raw += chunk
|
||||
data = bytes(self.raw[:n])
|
||||
del self.raw[:n]
|
||||
return data
|
||||
async def _flush(self):
|
||||
for data in self.protocol.data_to_send():
|
||||
if data:
|
||||
self.writer.write(data)
|
||||
await self.writer.drain()
|
||||
|
||||
async def read(self, n):
|
||||
while len(self.buffer) < n:
|
||||
b0, b1 = await self._raw(2)
|
||||
opcode, masked, length = b0 & 0x0F, b1 & 0x80, b1 & 0x7F
|
||||
if length == 126:
|
||||
length = struct.unpack(">H", await self._raw(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack(">Q", await self._raw(8))[0]
|
||||
mask = await self._raw(4) if masked else None
|
||||
payload = await self._raw(length)
|
||||
if mask:
|
||||
payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
|
||||
if opcode == 0x8:
|
||||
await self._frame(0x8, payload[:2])
|
||||
for frame in self.protocol.events_received():
|
||||
if frame.opcode in (Opcode.CONT, Opcode.TEXT, Opcode.BINARY):
|
||||
self.buffer += frame.data
|
||||
await self._flush() # pongs and the reply to a close frame
|
||||
if len(self.buffer) >= n:
|
||||
break
|
||||
if self.protocol.state is not State.OPEN:
|
||||
raise ConnectionClosed()
|
||||
if opcode == 0x9:
|
||||
await self._frame(0xA, payload)
|
||||
elif opcode in (0x0, 0x1, 0x2):
|
||||
self.buffer += payload
|
||||
chunk = await self.reader.read(65536)
|
||||
if not chunk:
|
||||
raise ConnectionClosed()
|
||||
self.protocol.receive_data(chunk)
|
||||
data = bytes(self.buffer[:n])
|
||||
del self.buffer[:n]
|
||||
return data
|
||||
|
||||
async def _frame(self, opcode, payload):
|
||||
n = len(payload)
|
||||
if n < 126:
|
||||
header = struct.pack(">BB", 0x80 | opcode, n)
|
||||
elif n < 0x10000:
|
||||
header = struct.pack(">BBH", 0x80 | opcode, 126, n)
|
||||
else:
|
||||
header = struct.pack(">BBQ", 0x80 | opcode, 127, n)
|
||||
self.writer.write(header + payload)
|
||||
await self.writer.drain()
|
||||
|
||||
async def write(self, data):
|
||||
await self._frame(0x2, data)
|
||||
self.protocol.send_binary(data)
|
||||
await self._flush()
|
||||
|
||||
async def close(self):
|
||||
if self.protocol.state is State.OPEN:
|
||||
self.protocol.send_close(CloseCode.NORMAL_CLOSURE)
|
||||
await self._flush()
|
||||
self.writer.close()
|
||||
|
||||
|
||||
# -- protocol -----------------------------------------------------------------------
|
||||
@@ -364,6 +361,7 @@ async def handle_client(reader, writer, config: Config):
|
||||
else:
|
||||
return
|
||||
await BoltConnection(transport, config).serve()
|
||||
await transport.close()
|
||||
except (ConnectionClosed, asyncio.IncompleteReadError, ConnectionResetError, BrokenPipeError):
|
||||
pass
|
||||
except Exception: # noqa: BLE001
|
||||
|
||||
Reference in new issue
Block a user