diff --git a/README.md b/README.md index f3d3e78..33acc53 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,12 @@ release. The versioned HTTP contract remains independent from straywild's save s network protocol. Each tagged deployment accepts room advertisements only from its matching game release so incompatible rooms are never presented as publicly available. +Existing NETfishing installations can be upgraded in place. The legacy +`netfishing_discovery` module, command name, and `NETFISHING_DISCOVERY_*` +environment variables remain accepted, while new installations should use the +Straywild names shown above. When both forms of an environment setting exist, +the Straywild value takes precedence. + ## Licensing Project-owned source code is licensed under the GNU General Public License, diff --git a/netfishing_discovery/__init__.py b/netfishing_discovery/__init__.py new file mode 100644 index 0000000..fc94ed5 --- /dev/null +++ b/netfishing_discovery/__init__.py @@ -0,0 +1,5 @@ +"""Compatibility package for pre-Straywild discovery installations.""" + +from straywild_discovery import __version__ + +__all__ = ["__version__"] diff --git a/netfishing_discovery/__main__.py b/netfishing_discovery/__main__.py new file mode 100644 index 0000000..33adb15 --- /dev/null +++ b/netfishing_discovery/__main__.py @@ -0,0 +1,7 @@ +"""Run the Straywild discovery service through its legacy module name.""" + +from straywild_discovery.server import main + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index d8ce045..c8693ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ classifiers = [ [project.scripts] straywild-discovery-server = "straywild_discovery.server:main" +netfishing-discovery-server = "straywild_discovery.server:main" [tool.setuptools.packages.find] -include = ["straywild_discovery*"] +include = ["straywild_discovery*", "netfishing_discovery*"] diff --git a/straywild_discovery/server.py b/straywild_discovery/server.py index 6ffa85d..8198592 100644 --- a/straywild_discovery/server.py +++ b/straywild_discovery/server.py @@ -36,6 +36,13 @@ TRAVERSAL_PACKET_PREFIX = b"straywild_TRAVERSAL_V1 " MAX_TRAVERSAL_PACKET_BYTES = 2048 +def _environment(primary: str, legacy: str, default: str) -> str: + """Read the Straywild setting first, then its NETfishing predecessor.""" + if primary in os.environ: + return os.environ[primary] + return os.getenv(legacy, default) + + @dataclass(frozen=True, slots=True) class ServerConfig: bind_host: str = "127.0.0.1" @@ -53,39 +60,72 @@ class ServerConfig: @classmethod def from_environment(cls) -> "ServerConfig": proxy_cidrs: list[ipaddress.IPv4Network | ipaddress.IPv6Network] = [] - raw_cidrs = os.getenv("straywild_DISCOVERY_TRUSTED_PROXY_CIDRS", "") + raw_cidrs = _environment( + "straywild_DISCOVERY_TRUSTED_PROXY_CIDRS", + "NETFISHING_DISCOVERY_TRUSTED_PROXY_CIDRS", + "", + ) for value in raw_cidrs.split(","): value = value.strip() if value: proxy_cidrs.append(ipaddress.ip_network(value, strict=False)) return cls( - bind_host=os.getenv("straywild_DISCOVERY_HOST", "127.0.0.1"), - bind_port=int(os.getenv("straywild_DISCOVERY_PORT", "7770")), - room_ttl_seconds=float(os.getenv("straywild_DISCOVERY_ROOM_TTL", "45")), + bind_host=_environment( + "straywild_DISCOVERY_HOST", "NETFISHING_DISCOVERY_HOST", "127.0.0.1" + ), + bind_port=int(_environment( + "straywild_DISCOVERY_PORT", "NETFISHING_DISCOVERY_PORT", "7770" + )), + room_ttl_seconds=float(_environment( + "straywild_DISCOVERY_ROOM_TTL", + "NETFISHING_DISCOVERY_ROOM_TTL", + "45", + )), max_rooms=int( - os.getenv("straywild_DISCOVERY_MAX_ROOMS", str(DEFAULT_MAX_ROOMS)) + _environment( + "straywild_DISCOVERY_MAX_ROOMS", + "NETFISHING_DISCOVERY_MAX_ROOMS", + str(DEFAULT_MAX_ROOMS), + ) ), max_rooms_per_address=int( - os.getenv( + _environment( "straywild_DISCOVERY_MAX_ROOMS_PER_ADDRESS", + "NETFISHING_DISCOVERY_MAX_ROOMS_PER_ADDRESS", str(DEFAULT_MAX_ROOMS_PER_ADDRESS), ) ), max_body_bytes=int( - os.getenv("straywild_DISCOVERY_MAX_BODY_BYTES", str(DEFAULT_MAX_BODY_BYTES)) + _environment( + "straywild_DISCOVERY_MAX_BODY_BYTES", + "NETFISHING_DISCOVERY_MAX_BODY_BYTES", + str(DEFAULT_MAX_BODY_BYTES), + ) ), trusted_proxy_cidrs=tuple(proxy_cidrs), - traversal_bind_host=os.getenv( - "straywild_DISCOVERY_TRAVERSAL_HOST", "0.0.0.0" + traversal_bind_host=_environment( + "straywild_DISCOVERY_TRAVERSAL_HOST", + "NETFISHING_DISCOVERY_TRAVERSAL_HOST", + "0.0.0.0", ), traversal_port=int( - os.getenv("straywild_DISCOVERY_TRAVERSAL_PORT", "7771") + _environment( + "straywild_DISCOVERY_TRAVERSAL_PORT", + "NETFISHING_DISCOVERY_TRAVERSAL_PORT", + "7771", + ) ), - traversal_public_host=os.getenv( - "straywild_DISCOVERY_TRAVERSAL_PUBLIC_HOST", "127.0.0.1" + traversal_public_host=_environment( + "straywild_DISCOVERY_TRAVERSAL_PUBLIC_HOST", + "NETFISHING_DISCOVERY_TRAVERSAL_PUBLIC_HOST", + "127.0.0.1", ), build_revision=( - os.getenv("straywild_DISCOVERY_BUILD_REVISION", "unknown").strip() + _environment( + "straywild_DISCOVERY_BUILD_REVISION", + "NETFISHING_DISCOVERY_BUILD_REVISION", + "unknown", + ).strip() or "unknown" )[:64], ) @@ -520,7 +560,14 @@ def _argument_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--host", help="bind address (overrides environment)") parser.add_argument("--port", type=int, help="bind port (overrides environment)") - parser.add_argument("--log-level", default=os.getenv("straywild_DISCOVERY_LOG_LEVEL", "INFO")) + parser.add_argument( + "--log-level", + default=_environment( + "straywild_DISCOVERY_LOG_LEVEL", + "NETFISHING_DISCOVERY_LOG_LEVEL", + "INFO", + ), + ) return parser diff --git a/straywild_discovery/social_registry.py b/straywild_discovery/social_registry.py index 7a5b15d..3028c33 100644 --- a/straywild_discovery/social_registry.py +++ b/straywild_discovery/social_registry.py @@ -15,6 +15,7 @@ from .registry import RoomAdvertisement, RoomNotFoundError, RoomRegistry, Valida PRESENCE_DOMAIN = "straywild_PRESENCE_V1:" +LEGACY_PRESENCE_DOMAIN = "NETFISHING_PRESENCE_V1:" INVITE_DOMAIN = "straywild_INVITE_V1:" SOCIAL_TOKEN_LENGTH = 64 MAX_SOCIAL_CHANNELS_PER_REQUEST = 200 @@ -108,7 +109,14 @@ class SocialRegistry: online = payload.get("online") if not isinstance(online, bool): raise ValidationError("online must be a boolean") - channels = [_capability_id(PRESENCE_DOMAIN, token) for token in write_tokens] + channels = [ + channel + for token in write_tokens + for channel in ( + _capability_id(PRESENCE_DOMAIN, token), + _capability_id(LEGACY_PRESENCE_DOMAIN, token), + ) + ] now = self._clock() with self._lock: self._purge_locked(now) diff --git a/tests/test_server_config.py b/tests/test_server_config.py new file mode 100644 index 0000000..559198b --- /dev/null +++ b/tests/test_server_config.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import os +import unittest +from unittest.mock import patch + +from straywild_discovery.server import ServerConfig + + +class ServerConfigCompatibilityTests(unittest.TestCase): + def test_legacy_environment_remains_accepted(self) -> None: + with patch.dict( + os.environ, + { + "NETFISHING_DISCOVERY_HOST": "127.0.0.9", + "NETFISHING_DISCOVERY_PORT": "7791", + "NETFISHING_DISCOVERY_BUILD_REVISION": "legacy-config", + }, + clear=True, + ): + config = ServerConfig.from_environment() + self.assertEqual(config.bind_host, "127.0.0.9") + self.assertEqual(config.bind_port, 7791) + self.assertEqual(config.build_revision, "legacy-config") + + def test_straywild_environment_wins_over_legacy_names(self) -> None: + with patch.dict( + os.environ, + { + "straywild_DISCOVERY_HOST": "127.0.0.8", + "NETFISHING_DISCOVERY_HOST": "127.0.0.9", + "straywild_DISCOVERY_PORT": "7792", + "NETFISHING_DISCOVERY_PORT": "7791", + }, + clear=True, + ): + config = ServerConfig.from_environment() + self.assertEqual(config.bind_host, "127.0.0.8") + self.assertEqual(config.bind_port, 7792) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_social_registry.py b/tests/test_social_registry.py index 1b65633..a8439ee 100644 --- a/tests/test_social_registry.py +++ b/tests/test_social_registry.py @@ -77,6 +77,27 @@ class SocialRegistryTests(unittest.TestCase): [], ) + def test_presence_keeps_pre_rebrand_friend_channels_reachable(self) -> None: + write_token = "c" * 64 + legacy_channel = capability_id("NETFISHING_PRESENCE_V1:", write_token) + channels = self.social.publish_presence( + "203.0.113.10", + { + "write_tokens": [write_token], + "online": True, + "display_name": "Voyager", + "room_id": self.room.room_id, + "game_version": GAME_VERSION, + "protocol_version": PROTOCOL_VERSION, + }, + ) + self.assertIn(legacy_channel, channels) + presence = self.social.query_presence( + [legacy_channel], GAME_VERSION, PROTOCOL_VERSION, self.rooms + ) + self.assertEqual(len(presence), 1) + self.assertEqual(presence[0]["channel"], legacy_channel) + def test_invitation_requires_a_current_live_poll(self) -> None: inbox_token = "b" * 64 with self.assertRaisesRegex(