fix(relay): secure reconnect and room ownership
This commit is contained in:
@@ -90,6 +90,7 @@ if python3 scripts/check_build_workflow.py &&
|
||||
python3 scripts/check_workflow_action_pins.py &&
|
||||
python3 scripts/test_check_workflow_action_pins.py &&
|
||||
python3 scripts/test_check_codegen.py &&
|
||||
python3 scripts/test_generate_relay_protocol.py &&
|
||||
python3 scripts/test_format_native.py &&
|
||||
python3 scripts/check_update_packages_workflow.py &&
|
||||
python3 scripts/test_pubspec_version.py &&
|
||||
|
||||
@@ -11,17 +11,41 @@ SPEC_PATH = ROOT / "relay_protocol.json"
|
||||
DART_PATH = ROOT / "lib/watch_together/services/relay_protocol.g.dart"
|
||||
GO_PATH = ROOT / "server/relay_protocol_gen.go"
|
||||
|
||||
SUPPORTED_ID_PATTERN = r"^[A-Za-z0-9_-]+$"
|
||||
|
||||
|
||||
def camel_to_pascal(value: str) -> str:
|
||||
return value[:1].upper() + value[1:]
|
||||
|
||||
|
||||
def validated_id_pattern(spec: dict) -> str:
|
||||
try:
|
||||
pattern = spec["idPattern"]
|
||||
except KeyError:
|
||||
raise ValueError("idPattern is required") from None
|
||||
if not isinstance(pattern, str):
|
||||
raise ValueError("idPattern must be a string")
|
||||
if pattern != SUPPORTED_ID_PATTERN:
|
||||
raise ValueError(
|
||||
f"unsupported idPattern {pattern!r}; expected {SUPPORTED_ID_PATTERN!r}"
|
||||
)
|
||||
return pattern
|
||||
|
||||
|
||||
def dart_source(spec: dict) -> str:
|
||||
id_pattern = validated_id_pattern(spec)
|
||||
lines = [
|
||||
"// Generated by scripts/generate_relay_protocol.py. Do not edit.",
|
||||
"",
|
||||
"abstract final class RelayProtocol {",
|
||||
]
|
||||
lines.extend(
|
||||
[
|
||||
f" static const int protocolVersion = {spec['protocolVersion']};",
|
||||
f" static const int legacyProtocolVersion = {spec['legacyProtocolVersion']};",
|
||||
"",
|
||||
]
|
||||
)
|
||||
for group in ("clientMessageTypes", "serverMessageTypes"):
|
||||
for name, value in spec[group].items():
|
||||
lines.append(f" static const String {name} = {value!r};")
|
||||
@@ -33,7 +57,7 @@ def dart_source(spec: dict) -> str:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
" static final RegExp _idPattern = RegExp(r'^[A-Za-z0-9_-]+$');",
|
||||
f" static final RegExp _idPattern = RegExp(r{id_pattern!r});",
|
||||
"",
|
||||
" static bool isValidSessionId(String value) =>",
|
||||
" value.isNotEmpty && value.length <= maxSessionIdLength && _idPattern.hasMatch(value);",
|
||||
@@ -48,6 +72,7 @@ def dart_source(spec: dict) -> str:
|
||||
|
||||
|
||||
def go_source(spec: dict) -> str:
|
||||
validated_id_pattern(spec)
|
||||
lines = [
|
||||
"// Code generated by scripts/generate_relay_protocol.py. DO NOT EDIT.",
|
||||
"",
|
||||
@@ -55,6 +80,13 @@ def go_source(spec: dict) -> str:
|
||||
"",
|
||||
"const (",
|
||||
]
|
||||
lines.extend(
|
||||
[
|
||||
f"\trelayProtocolVersion = {spec['protocolVersion']}",
|
||||
f"\tlegacyRelayProtocolVersion = {spec['legacyProtocolVersion']}",
|
||||
"",
|
||||
]
|
||||
)
|
||||
protocol_constants = []
|
||||
for group in ("clientMessageTypes", "serverMessageTypes"):
|
||||
protocol_constants.extend(
|
||||
@@ -109,8 +141,10 @@ def go_source(spec: dict) -> str:
|
||||
|
||||
def main() -> None:
|
||||
spec = json.loads(SPEC_PATH.read_text(encoding="utf-8"))
|
||||
DART_PATH.write_text(dart_source(spec), encoding="utf-8")
|
||||
GO_PATH.write_text(go_source(spec), encoding="utf-8")
|
||||
dart_output = dart_source(spec)
|
||||
go_output = go_source(spec)
|
||||
DART_PATH.write_text(dart_output, encoding="utf-8")
|
||||
GO_PATH.write_text(go_output, encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import copy
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import generate_relay_protocol as generator
|
||||
|
||||
|
||||
class RelayProtocolGeneratorTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.spec = json.loads(generator.SPEC_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
def test_supported_pattern_renders_both_targets(self) -> None:
|
||||
dart_output = generator.dart_source(copy.deepcopy(self.spec))
|
||||
go_output = generator.go_source(copy.deepcopy(self.spec))
|
||||
|
||||
self.assertIn(
|
||||
f"RegExp(r{generator.SUPPORTED_ID_PATTERN!r})",
|
||||
dart_output,
|
||||
)
|
||||
self.assertIn("func validRelayID(value string, maxLength int) bool", go_output)
|
||||
|
||||
def test_changed_pattern_fails_before_writing_either_target(self) -> None:
|
||||
changed_spec = copy.deepcopy(self.spec)
|
||||
changed_spec["idPattern"] = r"^[A-Za-z0-9_.-]+$"
|
||||
|
||||
for renderer in (generator.dart_source, generator.go_source):
|
||||
with self.subTest(renderer=renderer.__name__):
|
||||
with self.assertRaisesRegex(ValueError, "idPattern"):
|
||||
renderer(changed_spec)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
spec_path = root / "relay_protocol.json"
|
||||
dart_path = root / "relay_protocol.g.dart"
|
||||
go_path = root / "relay_protocol_gen.go"
|
||||
spec_path.write_text(json.dumps(changed_spec), encoding="utf-8")
|
||||
dart_path.write_text("dart sentinel\n", encoding="utf-8")
|
||||
go_path.write_text("go sentinel\n", encoding="utf-8")
|
||||
|
||||
with (
|
||||
mock.patch.object(generator, "SPEC_PATH", spec_path),
|
||||
mock.patch.object(generator, "DART_PATH", dart_path),
|
||||
mock.patch.object(generator, "GO_PATH", go_path),
|
||||
):
|
||||
with self.assertRaisesRegex(ValueError, "idPattern"):
|
||||
generator.main()
|
||||
|
||||
self.assertEqual(dart_path.read_text(encoding="utf-8"), "dart sentinel\n")
|
||||
self.assertEqual(go_path.read_text(encoding="utf-8"), "go sentinel\n")
|
||||
|
||||
def test_missing_pattern_is_rejected(self) -> None:
|
||||
spec = copy.deepcopy(self.spec)
|
||||
del spec["idPattern"]
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "idPattern is required"):
|
||||
generator.validated_id_pattern(spec)
|
||||
|
||||
def test_non_string_pattern_is_rejected(self) -> None:
|
||||
for value in (None, 42, [generator.SUPPORTED_ID_PATTERN]):
|
||||
with self.subTest(value=value):
|
||||
spec = copy.deepcopy(self.spec)
|
||||
spec["idPattern"] = value
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "idPattern must be a string"):
|
||||
generator.validated_id_pattern(spec)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user