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:
Bart BroereandClaude Opus 5.5 committed 2026-10-21 11:27:25 +02:00
1 parent 3e8e498b56
commit f0fb9ddc74
18 files changed
+189 -162

No files matched your search

+48 -50
View File
@@ -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