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,10 @@
from .content_range import ContentRangeHandler
from .directory import DirectoryHandler
from .error import ErrorHandler
__all__ = (
"ContentRangeHandler",
"DirectoryHandler",
"ErrorHandler",
)
@@ -0,0 +1,76 @@
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from sanic.exceptions import (
HeaderNotFound,
InvalidRangeType,
RangeNotSatisfiable,
)
from sanic.models.protocol_types import Range
if TYPE_CHECKING:
from sanic import Request
class ContentRangeHandler(Range):
"""Parse and process the incoming request headers to extract the content range information.
Args:
request (Request): The incoming request object.
stats (os.stat_result): The stats of the file being served.
""" # noqa: E501
__slots__ = ("start", "end", "size", "total", "headers")
def __init__(self, request: Request, stats: os.stat_result) -> None:
self.total = stats.st_size
_range = request.headers.getone("range", None)
if _range is None:
raise HeaderNotFound("Range Header Not Found")
unit, _, value = tuple(map(str.strip, _range.partition("=")))
if unit != "bytes":
raise InvalidRangeType(
"{} is not a valid Range Type".format(unit), self
)
start_b, _, end_b = tuple(map(str.strip, value.partition("-")))
try:
self.start = int(start_b) if start_b else None
except ValueError:
raise RangeNotSatisfiable(
"'{}' is invalid for Content Range".format(start_b), self
)
try:
self.end = int(end_b) if end_b else None
except ValueError:
raise RangeNotSatisfiable(
"'{}' is invalid for Content Range".format(end_b), self
)
if self.end is None:
if self.start is None:
raise RangeNotSatisfiable(
"Invalid for Content Range parameters", self
)
else:
# this case represents `Content-Range: bytes 5-`
self.end = self.total - 1
else:
if self.start is None:
# this case represents `Content-Range: bytes -5`
self.start = self.total - self.end
self.end = self.total - 1
if self.start > self.end:
raise RangeNotSatisfiable(
"Invalid for Content Range parameters", self
)
self.size = self.end - self.start + 1
self.headers = {
"Content-Range": "bytes %s-%s/%s"
% (self.start, self.end, self.total)
}
def __bool__(self):
return hasattr(self, "size") and self.size > 0
@@ -0,0 +1,156 @@
from __future__ import annotations
from collections.abc import Iterable, Sequence
from datetime import datetime
from operator import itemgetter
from pathlib import Path
from stat import S_ISDIR
from typing import cast
from urllib.parse import unquote
from sanic.exceptions import NotFound
from sanic.pages.directory_page import DirectoryPage, FileInfo
from sanic.request import Request
from sanic.response import file, html, redirect
def _is_path_within_root(path: Path, root: Path) -> bool:
"""Check if a path (after resolution) is within the root directory.
Returns False for:
- Broken symlinks (cannot be resolved)
- Paths that resolve outside the root directory
- Any errors during resolution
"""
try:
resolved = path.resolve()
resolved.relative_to(root.resolve())
except (ValueError, OSError, RuntimeError):
return False
else:
return True
class DirectoryHandler:
"""Serve files from a directory.
Args:
uri (str): The URI to serve the files at.
directory (Path): The directory to serve files from.
directory_view (bool): Whether to show a directory listing or not.
index (str | Sequence[str] | None): The index file(s) to
serve if the directory is requested. Defaults to None.
root_path (Optional[Path]): The root path for security checks.
Symlinks resolving outside this path will be hidden from
directory listings. Defaults to directory if not specified.
follow_external_symlink_files (bool): Whether to show file symlinks
pointing outside root in directory listings. Defaults to False.
follow_external_symlink_dirs (bool): Whether to show directory symlinks
pointing outside root in directory listings. Defaults to False.
"""
def __init__(
self,
uri: str,
directory: Path,
directory_view: bool = False,
index: str | Sequence[str] | None = None,
root_path: Path | None = None,
follow_external_symlink_files: bool = False,
follow_external_symlink_dirs: bool = False,
) -> None:
if isinstance(index, str):
index = [index]
elif index is None:
index = []
self.base = uri.strip("/")
self.directory = directory
self.directory_view = directory_view
self.index = tuple(index)
self.root_path = root_path if root_path is not None else directory
self.follow_external_symlink_files = follow_external_symlink_files
self.follow_external_symlink_dirs = follow_external_symlink_dirs
async def handle(self, request: Request, path: str):
"""Handle the request.
Args:
request (Request): The incoming request object.
path (str): The path to the file to serve.
Raises:
NotFound: If the file is not found.
IsADirectoryError: If the path is a directory and directory_view is False.
Returns:
Response: The response object.
""" # noqa: E501
current = unquote(path).strip("/")[len(self.base) :].strip("/") # noqa: E203
for file_name in self.index:
index_file = self.directory / current / file_name
if index_file.is_file():
return await file(index_file)
if self.directory_view:
return self._index(
self.directory / current, path, request.app.debug
)
if self.index:
raise NotFound("File not found")
raise IsADirectoryError(f"{self.directory.as_posix()} is a directory")
def _index(self, location: Path, path: str, debug: bool):
# Remove empty path elements, append slash
if "//" in path or not path.endswith("/"):
return redirect(
"/" + "".join([f"{p}/" for p in path.split("/") if p])
)
# Render file browser
page = DirectoryPage(self._iter_files(location), path, debug)
return html(page.render())
def _prepare_file(self, path: Path) -> dict[str, int | str] | None:
try:
stat = path.stat()
except OSError:
return None
modified = (
datetime.fromtimestamp(stat.st_mtime)
.isoformat()[:19]
.replace("T", " ")
)
is_dir = S_ISDIR(stat.st_mode)
icon = "📁" if is_dir else "📄"
file_name = path.name
if is_dir:
file_name += "/"
return {
"priority": is_dir * -1,
"file_name": file_name,
"icon": icon,
"file_access": modified,
"file_size": stat.st_size,
}
def _iter_files(self, location: Path) -> Iterable[FileInfo]:
prepared = []
for f in location.iterdir():
if f.is_symlink() and not _is_path_within_root(f, self.root_path):
# External symlink - check if allowed based on type
try:
is_dir = f.resolve().is_dir()
except OSError:
continue # Broken symlink
if is_dir and not self.follow_external_symlink_dirs:
continue
if not is_dir and not self.follow_external_symlink_files:
continue
file_info = self._prepare_file(f)
if file_info is not None:
prepared.append(file_info)
for item in sorted(prepared, key=itemgetter("priority", "file_name")):
del item["priority"]
yield cast(FileInfo, item)
@@ -0,0 +1,203 @@
from __future__ import annotations
from sanic.errorpages import BaseRenderer, TextRenderer, exception_response
from sanic.exceptions import ServerError
from sanic.log import error_logger
from sanic.models.handler_types import RouteHandler
from sanic.request.types import Request
from sanic.response import text
from sanic.response.types import HTTPResponse
class ErrorHandler:
"""Process and handle all uncaught exceptions.
This error handling framework is built into the core that can be extended
by the developers to perform a wide range of tasks from recording the error
stats to reporting them to an external service that can be used for
realtime alerting system.
Args:
base (BaseRenderer): The renderer to use for the error pages.
""" # noqa: E501
def __init__(
self,
base: type[BaseRenderer] = TextRenderer,
):
self.cached_handlers: dict[
tuple[type[BaseException], str | None], RouteHandler | None
] = {}
self.debug = False
self.base = base
def _full_lookup(self, exception, route_name: str | None = None):
return self.lookup(exception, route_name)
def _add(
self,
key: tuple[type[BaseException], str | None],
handler: RouteHandler,
) -> None:
if key in self.cached_handlers:
exc, name = key
if name is None:
name = "__ALL_ROUTES__"
message = (
f"Duplicate exception handler definition on: route={name} "
f"and exception={exc}"
)
raise ServerError(message)
self.cached_handlers[key] = handler
def add(self, exception, handler, route_names: list[str] | None = None):
"""Add a new exception handler to an already existing handler object.
Args:
exception (sanic.exceptions.SanicException or Exception): Type
of exception that needs to be handled.
handler (function): Reference to the function that will
handle the exception.
Returns:
None
""" # noqa: E501
if route_names:
for route in route_names:
self._add((exception, route), handler)
else:
self._add((exception, None), handler)
def lookup(self, exception, route_name: str | None = None):
"""Lookup the existing instance of `ErrorHandler` and fetch the registered handler for a specific type of exception.
This method leverages a dict lookup to speedup the retrieval process.
Args:
exception (sanic.exceptions.SanicException or Exception): Type
of exception.
Returns:
Registered function if found, ``None`` otherwise.
""" # noqa: E501
exception_class = type(exception)
for name in (route_name, None):
exception_key = (exception_class, name)
handler = self.cached_handlers.get(exception_key)
if handler:
return handler
for name in (route_name, None):
for ancestor in type.mro(exception_class):
exception_key = (ancestor, name)
if exception_key in self.cached_handlers:
handler = self.cached_handlers[exception_key]
self.cached_handlers[(exception_class, route_name)] = (
handler
)
return handler
if ancestor is BaseException:
break
self.cached_handlers[(exception_class, route_name)] = None
handler = None
return handler
_lookup = _full_lookup
def response(self, request, exception):
"""Fetch and executes an exception handler and returns a response object.
Args:
request (sanic.request.Request): Instance of the request.
exception (sanic.exceptions.SanicException or Exception): Exception to handle.
Returns:
Wrap the return value obtained from the `default` function or the registered handler for that type of exception.
""" # noqa: E501
route_name = request.name if request else None
handler = self._lookup(exception, route_name)
response = None
try:
if handler:
response = handler(request, exception)
if response is None:
response = self.default(request, exception)
except Exception:
try:
url = repr(request.url)
except AttributeError: # no cov
url = "unknown"
response_message = (
'Exception raised in exception handler "%s" for uri: %s'
)
error_logger.exception(response_message, handler.__name__, url)
if self.debug:
return text(response_message % (handler.__name__, url), 500)
else:
return text("An error occurred while handling an error", 500)
return response
def default(self, request: Request, exception: Exception) -> HTTPResponse:
"""Provide a default behavior for the objects of ErrorHandler.
If a developer chooses to extend the ErrorHandler, they can
provide a custom implementation for this method to behave in a way
they see fit.
Args:
request (sanic.request.Request): Incoming request.
exception (sanic.exceptions.SanicException or Exception): Exception object.
Returns:
HTTPResponse: The response object.
Examples:
```python
class CustomErrorHandler(ErrorHandler):
def default(self, request: Request, exception: Exception) -> HTTPResponse:
# Custom logic for handling the exception and creating a response
custom_response = my_custom_logic(request, exception)
return custom_response
app = Sanic("MyApp", error_handler=CustomErrorHandler())
```
""" # noqa: E501
self.log(request, exception)
fallback = request.app.config.FALLBACK_ERROR_FORMAT
return exception_response(
request,
exception,
debug=self.debug,
base=self.base,
fallback=fallback,
)
@staticmethod
def log(request: Request, exception: Exception) -> None:
"""Logs information about an incoming request and the associated exception.
Args:
request (Request): The incoming request to be logged.
exception (Exception): The exception that occurred during the handling of the request.
Returns:
None
""" # noqa: E501
quiet = getattr(exception, "quiet", False)
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
if quiet is False or noisy is True:
try:
url = repr(request.url)
except AttributeError: # no cov
url = "unknown"
error_logger.exception(
"Exception occurred while handling uri: %s", url
)