chore: rename project coven → hack-house ⛧

Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-05-30 13:29:14 -07:00
parent 82a04f3e12
commit bb1d662ee1
2730 changed files with 712933 additions and 46 deletions
@@ -0,0 +1,260 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from sanic.exceptions import RequestCancelled
if TYPE_CHECKING:
from sanic.app import Sanic
import asyncio
from asyncio.transports import Transport
from time import monotonic as current_time
from sanic.log import error_logger
from sanic.models.server_types import ConnInfo, Signal
class SanicProtocol(asyncio.Protocol):
__slots__ = (
"app",
# event loop, connection
"loop",
"transport",
"connections",
"conn_info",
"signal",
"_can_write",
"_time",
"_task",
"_unix",
"_data_received",
)
def __init__(
self,
*,
loop,
app: Sanic,
signal=None,
connections=None,
unix=None,
**kwargs,
):
asyncio.set_event_loop(loop)
self.loop = loop
self.app: Sanic = app
self.signal = signal or Signal()
self.transport: Transport | None = None
self.connections = connections if connections is not None else set()
self.conn_info: ConnInfo | None = None
self._can_write = asyncio.Event()
self._can_write.set()
self._unix = unix
self._time = 0.0 # type: float
self._task: asyncio.Task | None = None
self._data_received = asyncio.Event()
@property
def ctx(self):
if self.conn_info is not None:
return self.conn_info.ctx
else:
return None
async def send(self, data):
"""
Generic data write implementation with backpressure control.
"""
await self._can_write.wait()
if self.transport.is_closing():
raise RequestCancelled
self.transport.write(data)
self._time = current_time()
async def receive_more(self):
"""
Wait until more data is received into the Server protocol's buffer
"""
self.transport.resume_reading()
self._data_received.clear()
await self._data_received.wait()
def close(self, timeout: float | None = None):
"""
Attempt close the connection.
"""
if self.transport is None or self.transport.is_closing():
# do not attempt to close again, already aborted or closing
return
# Check if write is already paused _before_ close() is called.
write_was_paused = not self._can_write.is_set()
# Trigger the UVLoop Stream Transport Close routine
# Causes a call to connection_lost where further cleanup occurs
# Close may fully close the connection now, but if there is still
# data in the libuv buffer, then close becomes an async operation
self.transport.close()
try:
# Check write-buffer data left _after_ close is called.
# in UVLoop, get the data in the libuv transport write-buffer
data_left = self.transport.get_write_buffer_size()
# Some asyncio implementations don't support get_write_buffer_size
except (AttributeError, NotImplementedError):
data_left = 0
if write_was_paused or data_left > 0:
# don't call resume_writing here, it gets called by the transport
# to unpause the protocol when it is ready for more data
# Schedule the async close checker, to close the connection
# after the transport is done, and clean everything up.
if timeout is None:
# This close timeout needs to be less than the graceful
# shutdown timeout. The graceful shutdown _could_ be waiting
# for this transport to close before shutting down the app.
timeout = self.app.config.GRACEFUL_TCP_CLOSE_TIMEOUT
# This is 5s by default.
else:
# Schedule the async close checker but with no timeout,
# this will ensure abort() is called if required.
if timeout is None:
timeout = 0
self.loop.call_soon(
_async_protocol_transport_close,
self,
self.loop,
timeout,
)
def abort(self):
"""
Force close the connection.
"""
# Cause a call to connection_lost where further cleanup occurs
if self.transport:
self.transport.abort()
self.transport = None
# asyncio.Protocol API Callbacks #
# ------------------------------ #
def connection_made(self, transport):
"""
Generic connection-made, with no connection_task, and no recv_buffer.
Override this for protocol-specific connection implementations.
"""
try:
transport.set_write_buffer_limits(low=16384, high=65536)
self.connections.add(self)
self.transport = transport
self.conn_info = ConnInfo(self.transport, unix=self._unix)
except Exception:
error_logger.exception("protocol.connect_made")
def connection_lost(self, exc):
"""
This is a callback handler that is called from the asyncio
transport layer implementation (eg, UVLoop's UVStreamTransport).
It is scheduled to be called async after the transport has closed.
When data is still in the send buffer, this call to connection_lost
will be delayed until _after_ the buffer is finished being sent.
So we can use this callback as a confirmation callback
that the async write-buffer transfer is finished.
"""
try:
self.connections.discard(self)
# unblock the send queue if it is paused,
# this allows the route handler to see
# the CancelledError exception
self.resume_writing()
self.conn_info.lost = True
if self._task:
self._task.cancel()
except BaseException:
error_logger.exception("protocol.connection_lost")
def pause_writing(self):
self._can_write.clear()
def resume_writing(self):
self._can_write.set()
def data_received(self, data: bytes):
try:
self._time = current_time()
if not data:
return self.close()
if self._data_received:
self._data_received.set()
except BaseException:
error_logger.exception("protocol.data_received")
def _async_protocol_transport_close(
protocol: SanicProtocol,
loop: asyncio.AbstractEventLoop,
timeout: float,
):
"""
This function is scheduled to be called after close() is called.
It checks that the transport has shut down properly, or waits
for any remaining data to be sent, and aborts after a timeout.
This is required if the transport is closed while there is an async
large async transport write operation in progress.
This is observed when NGINX reverse-proxy is the client.
"""
if protocol.transport is None:
# abort() is the only method that can make
# protocol.transport be None, so abort was already called
return
# protocol.connection_lost does not set protocol.transport to None
# so to detect it a different way with conninfo.lost
elif protocol.conn_info is not None and protocol.conn_info.lost:
# Terminus. Most connections finish the protocol here!
# Connection_lost callback was executed already,
# so transport did complete and close itself properly.
# No need to call abort().
# This is the last part of cleanup to do
# that is not done by connection_lost handler.
# Ensure transport is cleaned up by GC.
protocol.transport = None
return
elif not protocol.transport.is_closing():
raise RuntimeError(
"You must call transport.close() before "
"protocol._async_transport_close() runs."
)
write_is_paused = not protocol._can_write.is_set()
try:
# in UVLoop, get the data in the libuv write-buffer
data_left = protocol.transport.get_write_buffer_size()
# Some asyncio implementations don't support get_write_buffer_size
except (AttributeError, NotImplementedError):
data_left = 0
if write_is_paused or data_left > 0:
# don't need to call resume_writing here to unpause
if timeout <= 0:
# timeout is 0 or less, so we can simply abort now
loop.call_soon(SanicProtocol.abort, protocol)
else:
next_check_interval = min(timeout, 0.1)
next_check_timeout = timeout - next_check_interval
loop.call_later(
# Recurse back in after the timeout, to check again
next_check_interval,
# this next time with reduced timeout.
_async_protocol_transport_close,
protocol,
loop,
next_check_timeout,
)
else:
# Not paused, and no data left in the buffer, but transport
# is still open, connection_lost has not been called yet.
# We can call abort() to fix that.
loop.call_soon(SanicProtocol.abort, protocol)
@@ -0,0 +1,360 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
from sanic.http.constants import HTTP
from sanic.http.http3 import Http3
from sanic.touchup.meta import TouchUpMeta
if TYPE_CHECKING:
from sanic.app import Sanic
from asyncio import CancelledError
from time import monotonic as current_time
from sanic.exceptions import (
RequestCancelled,
RequestTimeout,
ServiceUnavailable,
)
from sanic.http import Http, Stage
from sanic.log import (
Colors,
access_logger,
error_logger,
logger,
websockets_logger,
)
from sanic.models.server_types import ConnInfo
from sanic.request import Request
from sanic.server.protocols.base_protocol import SanicProtocol
ConnectionProtocol = type("ConnectionProtocol", (), {})
try:
from aioquic.asyncio import QuicConnectionProtocol
from aioquic.h3.connection import H3_ALPN, H3Connection
from aioquic.quic.events import (
DatagramFrameReceived,
ProtocolNegotiated,
QuicEvent,
)
ConnectionProtocol = QuicConnectionProtocol
except ModuleNotFoundError: # no cov
...
class HttpProtocolMixin:
__slots__ = ()
__version__: HTTP
def _setup_connection(self, *args, **kwargs):
self._http = self.HTTP_CLASS(self, *args, **kwargs)
self._time = current_time()
try:
self.check_timeouts()
except AttributeError:
...
def _setup(self):
self.request: Request | None = None
self.access_log = self.app.config.ACCESS_LOG
self.request_handler = self.app.handle_request
self.error_handler = self.app.error_handler
self.request_timeout = self.app.config.REQUEST_TIMEOUT
self.response_timeout = self.app.config.RESPONSE_TIMEOUT
self.keep_alive_timeout = self.app.config.KEEP_ALIVE_TIMEOUT
self.request_max_size = self.app.config.REQUEST_MAX_SIZE
self.request_class = self.app.request_class or Request
@property
def http(self):
if not hasattr(self, "_http"):
return None
return self._http
@property
def version(self):
return self.__class__.__version__
class HttpProtocol(HttpProtocolMixin, SanicProtocol, metaclass=TouchUpMeta):
"""
This class provides implements the HTTP 1.1 protocol on top of our
Sanic Server transport
"""
HTTP_CLASS = Http
__touchup__ = (
"send",
"connection_task",
)
__version__ = HTTP.VERSION_1
__slots__ = (
# request params
"request",
# request config
"request_handler",
"request_timeout",
"response_timeout",
"keep_alive_timeout",
"request_max_size",
"request_class",
"error_handler",
# enable or disable access log purpose
"access_log",
# connection management
"state",
"url",
"_handler_task",
"_http",
"_exception",
"recv_buffer",
"_callback_check_timeouts",
)
def __init__(
self,
*,
loop,
app: Sanic,
signal=None,
connections=None,
state=None,
unix=None,
**kwargs,
):
super().__init__(
loop=loop,
app=app,
signal=signal,
connections=connections,
unix=unix,
)
self.url = None
self.state = state if state else {}
self._setup()
if "requests_count" not in self.state:
self.state["requests_count"] = 0
self._exception = None
self._callback_check_timeouts = None
async def connection_task(self): # no cov
"""
Run a HTTP connection.
Timeouts and some additional error handling occur here, while most of
everything else happens in class Http or in code called from there.
"""
try:
self._setup_connection()
await self.app.dispatch(
"http.lifecycle.begin",
inline=True,
context={"conn_info": self.conn_info},
)
await self._http.http1()
except CancelledError:
pass
except Exception:
error_logger.exception("protocol.connection_task uncaught")
finally:
if (
self.access_log
and self._http
and self.transport
and not self._http.upgrade_websocket
):
self.log_disconnect()
self._http = None
self._task = None
try:
self.close()
except BaseException:
error_logger.exception("Closing failed")
finally:
await self.app.dispatch(
"http.lifecycle.complete",
inline=True,
context={"conn_info": self.conn_info},
)
# Important to keep this Ellipsis here for the TouchUp module
...
def log_disconnect(self):
ip = self.transport.get_extra_info("peername")
req = self._http.request
res = self._http.response
extra = {
"status": res.status if res else str(self._http.stage),
"byte": "DISCONNECTED",
"host": f"{id(self):X}"[-5:-1] + "unx",
"request": "nil",
"duration": "",
}
if req is not None:
if ip := req.client_ip:
extra["host"] = f"{ip}:{req.port}"
extra["request"] = f"{req.method} {req.url}"
access_logger.info("", extra=extra)
def check_timeouts(self):
"""
Runs itself periodically to enforce any expired timeouts.
"""
try:
if not self._task:
return
duration = current_time() - self._time
stage = self._http.stage
if stage is Stage.IDLE and duration > self.keep_alive_timeout:
logger.debug("KeepAlive Timeout. Closing connection.")
elif stage is Stage.REQUEST and duration > self.request_timeout:
logger.debug("Request Timeout. Closing connection.")
self._http.exception = RequestTimeout("Request Timeout")
elif stage is Stage.HANDLER and self._http.upgrade_websocket:
websockets_logger.debug(
"Handling websocket. Timeouts disabled."
)
return
elif (
stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
and duration > self.response_timeout
):
logger.debug("Response Timeout. Closing connection.")
self._http.exception = ServiceUnavailable("Response Timeout")
else:
interval = (
min(
self.keep_alive_timeout,
self.request_timeout,
self.response_timeout,
)
/ 2
)
_interval = max(0.1, interval)
self._callback_check_timeouts = self.loop.call_later(
_interval, self.check_timeouts
)
return
if sys.version_info < (3, 14):
self._task.cancel("Cancel connection task with a timeout")
else:
self._task.cancel()
except Exception:
error_logger.exception("protocol.check_timeouts")
def close(self, timeout: float | None = None):
"""
Requires to prevent checking timeouts for closed connections
"""
if self._callback_check_timeouts:
self._callback_check_timeouts.cancel()
return super().close(timeout=timeout)
async def send(self, data): # no cov
"""
Writes HTTP data with backpressure control.
"""
await self._can_write.wait()
if self.transport.is_closing():
raise RequestCancelled
await self.app.dispatch(
"http.lifecycle.send",
inline=True,
context={"data": data},
)
self.transport.write(data)
self._time = current_time()
def close_if_idle(self) -> bool:
"""
Close the connection if a request is not being sent or received
:return: boolean - True if closed, false if staying open
"""
if self.http is None or self.http.stage is Stage.IDLE:
self.close()
return True
return False
# -------------------------------------------- #
# Only asyncio.Protocol callbacks below this
# -------------------------------------------- #
def connection_made(self, transport):
"""
HTTP-protocol-specific new connection handler
"""
try:
# TODO: Benchmark to find suitable write buffer limits
transport.set_write_buffer_limits(low=16384, high=65536)
self.connections.add(self)
self.transport = transport
self._task = self.loop.create_task(self.connection_task())
self.recv_buffer = bytearray()
self.conn_info = ConnInfo(self.transport, unix=self._unix)
except Exception:
error_logger.exception("protocol.connect_made")
def data_received(self, data: bytes):
try:
self._time = current_time()
if not data:
return self.close()
self.recv_buffer += data
if (
len(self.recv_buffer) >= self.app.config.REQUEST_BUFFER_SIZE
and self.transport
):
self.transport.pause_reading()
if self._data_received:
self._data_received.set()
except Exception:
error_logger.exception("protocol.data_received")
class Http3Protocol(HttpProtocolMixin, ConnectionProtocol): # type: ignore
HTTP_CLASS = Http3
__version__ = HTTP.VERSION_3
def __init__(self, *args, app: Sanic, **kwargs) -> None:
self.app = app
super().__init__(*args, **kwargs)
self._setup()
self._connection: H3Connection | None = None
def quic_event_received(self, event: QuicEvent) -> None:
logger.debug(
f"{Colors.BLUE}[quic_event_received]: "
f"{Colors.PURPLE}{event}{Colors.END}",
extra={"verbosity": 2},
)
if isinstance(event, ProtocolNegotiated):
self._setup_connection(transmit=self.transmit)
if event.alpn_protocol in H3_ALPN:
self._connection = H3Connection(
self._quic, enable_webtransport=True
)
elif isinstance(event, DatagramFrameReceived):
if event.data == b"quack":
self._quic.send_datagram_frame(b"quack-ack")
# pass event to the HTTP layer
if self._connection is not None:
for http_event in self._connection.handle_event(event):
self._http.http_event_received(http_event)
@property
def connection(self) -> H3Connection | None:
return self._connection
@@ -0,0 +1,230 @@
from collections.abc import Sequence
from typing import cast
try: # websockets >= 11.0
from websockets.protocol import State # type: ignore
from websockets.server import ServerProtocol # type: ignore
except ImportError: # websockets < 11.0
from websockets.connection import State
from websockets.server import ServerConnection as ServerProtocol
from websockets import http11
from websockets.datastructures import Headers as WSHeaders
from websockets.typing import Subprotocol
from sanic.exceptions import SanicException
from sanic.log import access_logger, websockets_logger
from sanic.request import Request
from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol
OPEN = State.OPEN
CLOSING = State.CLOSING
CLOSED = State.CLOSED
class WebSocketProtocol(HttpProtocol):
__slots__ = (
"websocket",
"websocket_timeout",
"websocket_max_size",
"websocket_ping_interval",
"websocket_ping_timeout",
"websocket_url",
"websocket_peer",
)
def __init__(
self,
*args,
websocket_timeout: float = 10.0,
websocket_max_size: int | None = None,
websocket_ping_interval: float | None = 20.0,
websocket_ping_timeout: float | None = 20.0,
**kwargs,
):
super().__init__(*args, **kwargs)
self.websocket: WebsocketImplProtocol | None = None
self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size
self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout
self.websocket_url: str | None = None
self.websocket_peer: str | None = None
def connection_lost(self, exc):
if self.websocket is not None:
self.websocket.connection_lost(exc)
super().connection_lost(exc)
self.log_websocket("CLOSE")
self.websocket_url = None
self.websocket_peer = None
def data_received(self, data):
if self.websocket is not None:
self.websocket.data_received(data)
else:
# Pass it to HttpProtocol handler first
# That will (hopefully) upgrade it to a websocket.
super().data_received(data)
def eof_received(self) -> bool | None:
if self.websocket is not None:
return self.websocket.eof_received()
else:
return False
def close(self, timeout: float | None = None):
# Called by HttpProtocol at the end of connection_task
# If we've upgraded to websocket, we do our own closing
if self.websocket is not None:
# Note, we don't want to use websocket.close()
# That is used for user's application code to send a
# websocket close packet. This is different.
self.websocket.end_connection(1001)
else:
super().close()
def close_if_idle(self):
# Called by Sanic Server when shutting down
# If we've upgraded to websocket, shut it down
if self.websocket is not None:
if self.websocket.ws_proto.state in (CLOSING, CLOSED):
return True
elif self.websocket.loop is not None:
self.websocket.loop.create_task(self.websocket.close(1001))
else:
self.websocket.end_connection(1001)
else:
return super().close_if_idle()
@staticmethod
def sanic_request_to_ws_request(request: Request):
return http11.Request(
path=request.path,
headers=WSHeaders(request.headers),
)
async def websocket_handshake(
self, request, subprotocols: Sequence[str] | None = None
):
# let the websockets package do the handshake with the client
try:
if subprotocols is not None:
# subprotocols can be a set or frozenset,
# but ServerProtocol needs a list
subprotocols = cast(
Sequence[Subprotocol] | None,
list(
[
Subprotocol(subprotocol)
for subprotocol in subprotocols
]
),
)
ws_proto = ServerProtocol(
max_size=self.websocket_max_size,
subprotocols=subprotocols,
state=OPEN,
logger=websockets_logger,
)
resp = ws_proto.accept(self.sanic_request_to_ws_request(request))
except Exception:
msg = (
"Failed to open a WebSocket connection.\n"
"See server log for more information.\n"
)
raise SanicException(msg, status_code=500)
if 100 <= resp.status_code <= 299:
first_line = (
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
).encode()
rbody = bytearray(first_line)
rbody += (
"".join([f"{k}: {v}\r\n" for k, v in resp.headers.items()])
).encode()
rbody += b"\r\n"
if resp.body:
rbody += resp.body
rbody += b"\r\n\r\n"
await super().send(rbody)
else:
raise SanicException(resp.body, resp.status_code)
self.websocket = WebsocketImplProtocol(
ws_proto,
ping_interval=self.websocket_ping_interval,
ping_timeout=self.websocket_ping_timeout,
close_timeout=self.websocket_timeout,
)
loop = (
request.transport.loop
if hasattr(request, "transport")
and hasattr(request.transport, "loop")
else None
)
await self.websocket.connection_made(self, loop=loop)
self.websocket_url = self._http.request.url
self.websocket_peer = f"{id(self):X}"[-5:-1] + "unx"
if ip := self._http.request.client_ip:
self.websocket_peer = f"{ip}:{self._http.request.port}"
self.log_websocket("OPEN")
return self.websocket
def log_websocket(self, message):
if not self.access_log or not self.websocket_url:
return
status = ""
close = ""
try:
# Can we get some useful statistics?
p = self.websocket.ws_proto
state = p.state
if state == CLOSED:
codes = {
1000: "NORMAL",
1001: "GOING AWAY",
1005: "NO STATUS",
1006: "ABNORMAL",
1011: "SERVER ERR",
}
if p.close_code == 1006:
message = "CLOSE_ABN"
scode = rcode = 1006 # Abnormal closure (disconnection)
sdesc = rdesc = ""
if p.close_sent:
scode = p.close_sent.code
sdesc = p.close_sent.reason
if p.close_rcvd:
rcode = p.close_rcvd.code
rdesc = p.close_rcvd.reason
# Use repr() to escape any control characters
sdesc = repr(sdesc[:256]) if sdesc else codes.get(scode, "")
rdesc = repr(rdesc[:256]) if rdesc else codes.get(rcode, "")
if p.close_rcvd_then_sent or scode == 1006:
status = rcode
close = (
f"{rdesc} from client"
if scode in (rcode, 1006)
else f"{rdesc} ▼▲ {scode} {sdesc}"
)
else:
status = scode
close = (
f"{sdesc} from server"
if rcode in (scode, 1006)
else f"{sdesc} ▲▼ {rcode} {rdesc}"
)
except AttributeError:
...
extra = {
"status": status,
"byte": close,
"host": self.websocket_peer,
"request": f" 🔌 {self.websocket_url}",
"duration": "",
}
access_logger.info(message, extra=extra)