Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-magic-wormhole-mailbox-server 
for openSUSE:Factory checked in at 2026-07-06 13:13:52
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-magic-wormhole-mailbox-server (Old)
 and      
/work/SRC/openSUSE:Factory/.python-magic-wormhole-mailbox-server.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-magic-wormhole-mailbox-server"

Mon Jul  6 13:13:52 2026 rev:10 rq:1364028 version:0.8.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-magic-wormhole-mailbox-server/python-magic-wormhole-mailbox-server.changes
        2026-04-26 21:14:00.528029180 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-magic-wormhole-mailbox-server.new.1982/python-magic-wormhole-mailbox-server.changes
      2026-07-06 13:15:18.252929873 +0200
@@ -1,0 +2,12 @@
+Mon Jun 29 08:42:27 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.8.0:
+  * Server header properly reports version
+  * introduce a ``"your-address"`` key to the ``"welcome"``
+    message (to reflect the IP address and port back, #63)
+  * non-numeric nameplates rejected with error (@meejah)
+  * more-complete sequence and state diagrams (@meejah)
+  * update Munin plugin shebang (@warner)
+  * Munin plugins open db read-only (@warner)
+
+-------------------------------------------------------------------

Old:
----
  magic_wormhole_mailbox_server-0.6.0.tar.gz

New:
----
  magic_wormhole_mailbox_server-0.8.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-magic-wormhole-mailbox-server.spec ++++++
--- /var/tmp/diff_new_pack.ZPmwA8/_old  2026-07-06 13:15:18.760947445 +0200
+++ /var/tmp/diff_new_pack.ZPmwA8/_new  2026-07-06 13:15:18.764947583 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-magic-wormhole-mailbox-server
-Version:        0.6.0
+Version:        0.8.0
 Release:        0
 Summary:        Key exchange and control message server for Magic-Wormhole
 License:        MIT

++++++ magic_wormhole_mailbox_server-0.6.0.tar.gz -> 
magic_wormhole_mailbox_server-0.8.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/magic_wormhole_mailbox_server-0.6.0/NEWS.md 
new/magic_wormhole_mailbox_server-0.8.0/NEWS.md
--- old/magic_wormhole_mailbox_server-0.6.0/NEWS.md     2026-02-13 
08:26:00.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/NEWS.md     2026-05-15 
19:20:26.000000000 +0200
@@ -1,4 +1,5 @@
 
+
 User-visible changes in "magic-wormhole-mailbox-server":
 
 ## Upcoming
@@ -6,6 +7,22 @@
 * (put release-notes here when merging / proposing a PR)
 
 
+## Release 0.8.0 (15-May-2026)
+
+* Server header properly reports version (#27)
+* introduce a ``"your-address"`` key to the ``"welcome"`` message (to reflect 
the IP address and port back, #63)
+
+
+## Release 0.7.0 (14-May-2026)
+
+* CI no longer tests Python 3.9 @meejah
+* CI now tests 3.14 @meejah
+* non-numeric nameplates rejected with error (@meejah)
+* more-complete sequence and state diagrams (@meejah)
+* update Munin plugin shebang (@warner)
+* Munin plugins open db read-only (@warner)
+
+
 ## Release 0.6.0 (13-Feb-2026)
 
 * CI no longer tests Python 3.8 (it is EOL)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/magic_wormhole_mailbox_server-0.6.0/PKG-INFO 
new/magic_wormhole_mailbox_server-0.8.0/PKG-INFO
--- old/magic_wormhole_mailbox_server-0.6.0/PKG-INFO    2026-02-13 
08:33:43.829119400 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/PKG-INFO    2026-05-15 
19:20:42.453664800 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: magic-wormhole-mailbox-server
-Version: 0.6.0
+Version: 0.8.0
 Summary: Securely transfer data between computers
 Home-page: https://github.com/warner/magic-wormhole-mailbox-server
 Author: Brian Warner
@@ -141,4 +141,4 @@
 
 This library is released under the MIT license, see LICENSE for details.
 
-This library is compatible with python3 (3.9 and higher).
+This library is compatible with python3 (3.10 and higher).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/magic_wormhole_mailbox_server-0.6.0/README.md 
new/magic_wormhole_mailbox_server-0.8.0/README.md
--- old/magic_wormhole_mailbox_server-0.6.0/README.md   2025-03-26 
20:28:12.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/README.md   2026-04-05 
09:03:32.000000000 +0200
@@ -99,4 +99,4 @@
 
 This library is released under the MIT license, see LICENSE for details.
 
-This library is compatible with python3 (3.9 and higher).
+This library is compatible with python3 (3.10 and higher).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/magic_wormhole_mailbox_server-0.6.0/docs/Makefile 
new/magic_wormhole_mailbox_server-0.8.0/docs/Makefile
--- old/magic_wormhole_mailbox_server-0.6.0/docs/Makefile       2019-03-04 
04:38:50.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/docs/Makefile       2026-04-05 
09:03:32.000000000 +0200
@@ -12,7 +12,12 @@
 help:
        @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 
-.PHONY: help Makefile
+.PHONY: help Makefile diagrams
+
+diagrams:
+       plantuml -Tpng happy-plant.seq
+       seqdiag --no-transparency --antialias -T png happy.seq
+       dot -Tpng states.dot > states.png
 
 # Catch-all target: route all unknown targets to Sphinx using the new
 # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/magic_wormhole_mailbox_server-0.6.0/docs/states.dot 
new/magic_wormhole_mailbox_server-0.8.0/docs/states.dot
--- old/magic_wormhole_mailbox_server-0.6.0/docs/states.dot     1970-01-01 
01:00:00.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/docs/states.dot     2026-04-05 
09:03:32.000000000 +0200
@@ -0,0 +1,74 @@
+/*digraph {
+    title [label="Mailbox\lServer Machine" style="dotted"]
+
+    start -> opened [label="open(side)"];
+
+    opened -> opened [label="open(side)"];
+    opened -> opened [label="add_message(sided_message)"];
+    opened -> closing [label="close(side, mood)"];
+
+    closing -> closing [label="close(side, mood)"];
+}
+*/
+
+
+// note: all messages have an "id" and a "type"
+// and the server sends back an "ack" for every one
+// but that ack etc isn't covered in these diagrams
+
+digraph {
+    node [fontname = "Source Sans Pro" fontsize = 22];
+    edge [fontname = "Source Code Pro" fontsize = 18 fontcolor=blue];
+    graph [fontname = "Source Sans Pro" fontsize = 22];
+
+    title [label="Mailbox Server" style="dotted" fontsize=32];
+
+    ranksep = 1;
+
+    start [label="START\n(perm=none)"];
+    start_permissions [label="START\n(perm=hashcash)"];
+    start_reconn [label="RECONNECT\n(mailbox, perm=none)"];
+    start_reconn_perm [label="RECONNECT\n(mailbox, perm=hashcash)"];
+    done [label="DONE\nmood=" shape=box style=filled];
+
+    {rank=same; start start_permissions start_reconn start_reconn_perm}
+    start [shape=box, style=bold];
+    start -> bound [label="bind(appid, side)"];
+
+    # blue, to match seqdiag section on "nameplate allocated"
+    bound [fillcolor=cadetblue1, style=filled];
+    have_nameplate [fillcolor=cadetblue1, style=filled];
+    claimed [fillcolor=cadetblue1, style=filled];
+
+    start_permissions [shape=box, style=bold];
+    start_permissions -> granted [label="submit_permissions()" fontcolor=red];
+    granted -> bound [label="bind(appid, side)"];
+
+    start_reconn [shape=box, style=bold];
+    start_reconn -> open [label="open(mailbox_id)\l-> messages: send(side, 
phase, body)\l"];
+
+    start_reconn_perm [shape=box, style=bold];
+    start_reconn_perm -> reconn_granted [label="submit_permissions()" 
fontcolor=red];
+    open [fillcolor=green style=filled];
+    reconn_granted -> open [label="open(mailbox_id)\l-> messages: send(side, 
phase, body)\l"];
+
+    bound -> have_nameplate [label="allocate()\l-> nameplate_id\l"]
+    # allocate() really does do a claim() .. but you have to call it 
explicitly too
+    have_nameplate -> claimed [label="claim(nameplate, side)\l-> mailbox_id\l" 
fontcolor=darkgreen]
+    have_nameplate -> done [label="release(nameplate)" fontcolor=red]
+
+    # ths is on the "join" side; they are told the nameplate number
+    bound -> claimed [label="claim(nameplate, side)\l-> mailbox_id\l" 
fontcolor=darkgreen]
+    claimed -> unclaimed [label="release(nameplate)" fontcolor=red]
+
+    # note: allowing two different paths to 'unclaimed' is I think
+    # _allowed_ currently by the server, but better to define it with
+    # juts one way probably.
+
+    unclaimed -> open [label="open(mailbox_id)\l-> messages: send(side, phase, 
body)\l"]
+    #claimed -> open [label="open(mailbox_id)\l-> send(all_messages)\l"]
+    #open -> open [label="release(nameplate)"]
+    open -> open [label="add_message(msg)\l-> send(side, phase, body)\l"]
+    open ->      done [label="close(mailbox_id)" fontcolor=red]
+    # XXX will get all message already in the box, how to represent?
+}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_active 
new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_active
--- old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_active  
2019-03-04 04:38:50.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_active  
2026-05-14 21:00:08.000000000 +0200
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#! /usr/bin/env python3
 
 """
 Use the following in /etc/munin/plugin-conf.d/wormhole :
@@ -32,11 +32,11 @@
 
 usagedbfile = os.environ["usagedb"]
 assert os.path.exists(usagedbfile)
-usage_db = sqlite3.connect(usagedbfile)
+usage_db = sqlite3.connect("file:%s?mode=ro" % usagedbfile, uri=True)
 
 channeldbfile = os.environ["channeldb"]
 assert os.path.exists(channeldbfile)
-channel_db = sqlite3.connect(channeldbfile)
+channel_db = sqlite3.connect("file:%s?mode=ro" % channeldbfile, uri=True)
 
 MINUTE = 60.0
 updated,rebooted = usage_db.execute("SELECT `updated`,`rebooted` FROM 
`current`").fetchone()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_errors 
new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_errors
--- old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_errors  
2019-03-04 04:38:50.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_errors  
2026-05-14 21:00:08.000000000 +0200
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#! /usr/bin/env python3
 
 """
 Use the following in /etc/munin/plugin-conf.d/wormhole :
@@ -31,7 +31,7 @@
 
 usagedbfile = os.environ["usagedb"]
 assert os.path.exists(usagedbfile)
-usage_db = sqlite3.connect(usagedbfile)
+usage_db = sqlite3.connect("file:%s?mode=ro" % usagedbfile, uri=True)
 
 MINUTE = 60.0
 updated,rebooted = usage_db.execute("SELECT `updated`,`rebooted` FROM 
`current`").fetchone()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_event_rate 
new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_event_rate
--- old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_event_rate      
2019-03-04 04:38:50.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_event_rate      
2026-05-14 21:00:08.000000000 +0200
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#! /usr/bin/env python3
 
 """
 Use the following in /etc/munin/plugin-conf.d/wormhole :
@@ -41,7 +41,7 @@
 
 usagedbfile = os.environ["usagedb"]
 assert os.path.exists(usagedbfile)
-usage_db = sqlite3.connect(usagedbfile)
+usage_db = sqlite3.connect("file:%s?mode=ro" % usagedbfile, uri=True)
 
 MINUTE = 60.0
 updated,rebooted = usage_db.execute("SELECT `updated`,`rebooted` FROM 
`current`").fetchone()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_events 
new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_events
--- old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_events  
2019-03-04 04:38:50.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_events  
2026-05-14 21:00:08.000000000 +0200
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#! /usr/bin/env python3
 
 """
 Use the following in /etc/munin/plugin-conf.d/wormhole :
@@ -40,7 +40,7 @@
 
 usagedbfile = os.environ["usagedb"]
 assert os.path.exists(usagedbfile)
-usage_db = sqlite3.connect(usagedbfile)
+usage_db = sqlite3.connect("file:%s?mode=ro" % usagedbfile, uri=True)
 
 MINUTE = 60.0
 updated,rebooted,blur = usage_db.execute(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_events_alltime 
new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_events_alltime
--- old/magic_wormhole_mailbox_server-0.6.0/misc/munin/wormhole_events_alltime  
2019-03-04 04:38:50.000000000 +0100
+++ new/magic_wormhole_mailbox_server-0.8.0/misc/munin/wormhole_events_alltime  
2026-05-14 21:00:08.000000000 +0200
@@ -1,4 +1,4 @@
-#! /usr/bin/env python
+#! /usr/bin/env python3
 
 """
 Use the following in /etc/munin/plugin-conf.d/wormhole :
@@ -40,7 +40,7 @@
 
 usagedbfile = os.environ["usagedb"]
 assert os.path.exists(usagedbfile)
-usage_db = sqlite3.connect(usagedbfile)
+usage_db = sqlite3.connect("file:%s?mode=ro" % usagedbfile, uri=True)
 
 MINUTE = 60.0
 updated,rebooted = usage_db.execute("SELECT `updated`,`rebooted` FROM 
`current`").fetchone()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/magic_wormhole_mailbox_server.egg-info/PKG-INFO
 
new/magic_wormhole_mailbox_server-0.8.0/src/magic_wormhole_mailbox_server.egg-info/PKG-INFO
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/magic_wormhole_mailbox_server.egg-info/PKG-INFO
 2026-02-13 08:33:43.000000000 +0100
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/magic_wormhole_mailbox_server.egg-info/PKG-INFO
 2026-05-15 19:20:42.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: magic-wormhole-mailbox-server
-Version: 0.6.0
+Version: 0.8.0
 Summary: Securely transfer data between computers
 Home-page: https://github.com/warner/magic-wormhole-mailbox-server
 Author: Brian Warner
@@ -141,4 +141,4 @@
 
 This library is released under the MIT license, see LICENSE for details.
 
-This library is compatible with python3 (3.9 and higher).
+This library is compatible with python3 (3.10 and higher).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/magic_wormhole_mailbox_server.egg-info/SOURCES.txt
 
new/magic_wormhole_mailbox_server-0.8.0/src/magic_wormhole_mailbox_server.egg-info/SOURCES.txt
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/magic_wormhole_mailbox_server.egg-info/SOURCES.txt
      2026-02-13 08:33:43.000000000 +0100
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/magic_wormhole_mailbox_server.egg-info/SOURCES.txt
      2026-05-15 19:20:42.000000000 +0200
@@ -11,6 +11,7 @@
 docs/conf.py
 docs/index.rst
 docs/server-protocol.md
+docs/states.dot
 docs/welcome.md
 misc/migrate_channel_db.py
 misc/migrate_usage_db.py
@@ -49,5 +50,6 @@
 src/wormhole_mailbox_server/test/test_stats.py
 src/wormhole_mailbox_server/test/test_util.py
 src/wormhole_mailbox_server/test/test_web.py
+src/wormhole_mailbox_server/test/test_websocket.py
 src/wormhole_mailbox_server/test/test_ws_client.py
 src/wormhole_mailbox_server/test/ws_client.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/_version.py 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/_version.py
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/_version.py 
    2026-02-13 08:33:43.829119400 +0100
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/_version.py 
    2026-05-15 19:20:42.457664700 +0200
@@ -8,11 +8,11 @@
 
 version_json = '''
 {
- "date": "2026-02-13T00:33:32-0700",
+ "date": "2026-05-15T11:20:33-0600",
  "dirty": false,
  "error": null,
- "full-revisionid": "348c907e14032a68301cf0a9017bc78e272c5094",
- "version": "0.6.0"
+ "full-revisionid": "67cdaf56113b37ac29d4bd35398bdd086191a41e",
+ "version": "0.8.0"
 }
 '''  # END VERSION_JSON
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/server.py 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/server.py
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/server.py   
    2025-10-08 18:43:07.000000000 +0200
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/server.py   
    2026-05-14 21:52:17.000000000 +0200
@@ -1,4 +1,4 @@
-import os, random, base64
+import os, random, base64, re
 from collections import namedtuple
 from twisted.python import log
 from twisted.application import service
@@ -6,6 +6,16 @@
 def generate_mailbox_id():
     return base64.b32encode(os.urandom(8)).lower().strip(b"=").decode("ascii")
 
+NAMEPLATE_RE = re.compile(r'^\d+$')
+
+def check_valid_nameplate(n):
+    if not isinstance(n, str):
+        raise ValueError("nameplate %r is %s not str" % (n, type(n)))
+    if len(n) > 40:
+        raise ValueError("nameplate %s .. is too long, %d>40" % (repr(n)[:50], 
len(n)))
+    if not NAMEPLATE_RE.search(n):
+        raise ValueError("nameplate %s has non-digits" % (n,))
+
 class CrowdedError(Exception):
     pass
 class ReclaimedError(Exception):
@@ -244,6 +254,7 @@
         #  * a mailbox 'side' will be attached, with opened=True
         assert isinstance(name, str), type(name)
         assert isinstance(side, str), type(side)
+        check_valid_nameplate(name)
         db = self._db
         row = db.execute("SELECT * FROM `nameplates`"
                          " WHERE `app_id`=? AND `name`=?",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/server_websocket.py
 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/server_websocket.py
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/server_websocket.py
     2025-10-08 18:43:07.000000000 +0200
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/server_websocket.py
     2026-05-15 19:14:29.000000000 +0200
@@ -2,7 +2,7 @@
 from twisted.internet import reactor
 from twisted.python import log
 from autobahn.twisted import websocket
-from .server import CrowdedError, ReclaimedError, SidedMessage
+from .server import CrowdedError, ReclaimedError, SidedMessage, 
check_valid_nameplate
 from .util import dict_to_bytes, bytes_to_dict
 
 # The WebSocket allows the client to send "commands" to the server, and the
@@ -108,16 +108,62 @@
         self._mailbox = None
         self._mailbox_id = None
         self._did_close = False
+        self._peer_addr_port = None
 
     def onConnect(self, request):
-        rv = self.factory.server
+        rv = self.factory._server
+        # Caddy uses capitalized headers like X-Real-IP and X-Real-Port, which
+        # you see if you forward Caddy to netcat. But the twisted/autobahn
+        # Request object lowercases everything.
+        #
+        # We only use this for assistance in NAT hole-punching, so it
+        # doesn't matter if the client is able to inject their own
+        # headers and spoof somebody else's IP address, they're only
+        # hurting themselves
+        if "x-real-ip" in request.headers:
+            peer_host = request.headers.get("x-real-ip") # either 1.2.3.4 or 
2600:..:1234
+            peer_port = request.headers.get("x-real-port")
+            # assume frontends like Caddy don't give us v4-in-v6 addrs
+            peer_type = "ipv6" if ":" in peer_host else "ipv4"
+        else:
+            peer = request.peer
+            peer_type = peer.split(":", maxsplit=1)[0]
+            peer_port = peer.rsplit(":", maxsplit=1)[-1]
+            peer_host = peer[len(peer_type)+1:(-len(peer_port)-1)]
+            # this gets me [tcp6, ::1, 53276] for a client using 
ws://localhost:4000/v1
+            #  or ws://[::1]:4000/v1
+            # and [tcp6, ::ffff:127.0.0.1, 53279] when using 
ws://127.0.0.1:4000/v1
+            if peer_type == "tcp6":
+                peer_type = "ipv6"
+                if peer_host.startswith("::ffff:"):
+                    peer_host = peer_host.rsplit(":", maxsplit=1)[-1]
+                    peer_type = "ipv4"
+            else:
+                peer_type = "ipv4"
+        self._peer_addr_port = (peer_type, peer_host, int(peer_port))
+
         if rv.get_log_requests():
-            log.msg(f"ws client connecting: {request.peer}")
+            v = 4 if peer_type == "ipv4" else 6
+            log.msg(f"ws client connecting: tcp{v}:{peer_host}:{peer_port}")
         self._reactor = self.factory.reactor
+        # can return (name, dict) or name here, where name is
+        # WebSocket subprotocol name and dict is extra headers (if
+        # provided) to send
+
+    def get_your_address(self):
+        (peer_type, peer_host, peer_port) = self._peer_addr_port
+        you = { "port": peer_port }
+        if peer_type == "ipv4":
+            you["ipv4"] = peer_host
+        elif peer_type == "ipv6":
+            you["ipv6"] = peer_host
+        return you
 
     def onOpen(self):
-        rv = self.factory.server
-        self.send("welcome", welcome=rv.get_welcome())
+        rv = self.factory._server
+        welcome = rv.get_welcome().copy()
+        welcome["your-address"] = self.get_your_address()
+        self.send("welcome", welcome=welcome)
 
     def onMessage(self, payload, isBinary):
         server_rx = time.time()
@@ -167,7 +213,7 @@
             raise Error("bind requires 'appid'")
         if "side" not in msg:
             raise Error("bind requires 'side'")
-        self._app = self.factory.server.get_app(msg["appid"])
+        self._app = self.factory._server.get_app(msg["appid"])
         self._side = msg["side"]
         client_version = msg.get("client_version", (None, None))
         # e.g. ("python", "0.xyz") . <=0.10.5 did not send client_version
@@ -196,7 +242,7 @@
             raise Error("only one claim per connection")
         self._did_claim = True
         nameplate_id = msg["nameplate"]
-        assert isinstance(nameplate_id, str), type(nameplate_id)
+        check_valid_nameplate(nameplate_id)
         self._nameplate_id = nameplate_id
         try:
             mailbox_id = self._app.claim_nameplate(nameplate_id, self._side,
@@ -212,6 +258,8 @@
             raise Error("only one release per connection")
         if "nameplate" in msg:
             if self._nameplate_id is not None:
+                # we only care about equality, don't bother with
+                # check_valid_nameplate()
                 if msg["nameplate"] != self._nameplate_id:
                     raise Error("release and claim must use same nameplate")
             nameplate_id = msg["nameplate"]
@@ -304,5 +352,9 @@
     def __init__(self, url, server):
         websocket.WebSocketServerFactory.__init__(self, url)
         self.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600)
-        self.server = server
+        # note: Autobahn uses "self.factory.server" for the Server
+        # version string, so we musn't use that as well.
+        self._server = server
+        from . import __version__
+        self.server = f"Magic Wormhole Mailbox {__version__}"
         self.reactor = reactor # for tests to control
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_server.py
 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_server.py
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_server.py
     2025-10-08 18:43:07.000000000 +0200
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_server.py
     2026-05-15 19:14:29.000000000 +0200
@@ -6,6 +6,7 @@
                       SidedMessage, CrowdedError, AppNamespace)
 from ..database import create_channel_db, create_usage_db
 
+npid = "1"
 
 class Server(_Util, ServerBase, unittest.TestCase):
     def test_apps(self):
@@ -323,17 +324,17 @@
         app = rv.get_app(APPID)
 
         # Exercise the first-vs-second newness tests
-        app.claim_nameplate("np-1", "side1", 1)
-        app.claim_nameplate("np-2", "side1", 1)
-        app.claim_nameplate("np-2", "side2", 2)
-        app.claim_nameplate("np-3", "side1", 60)
-        new_nameplates.add("np-3")
-        app.claim_nameplate("np-4", "side1", 1)
-        app.claim_nameplate("np-4", "side2", 60)
-        new_nameplates.add("np-4")
-        app.claim_nameplate("np-5", "side1", 60)
-        app.claim_nameplate("np-5", "side2", 61)
-        new_nameplates.add("np-5")
+        app.claim_nameplate("1", "side1", 1)
+        app.claim_nameplate("2", "side1", 1)
+        app.claim_nameplate("2", "side2", 2)
+        app.claim_nameplate("3", "side1", 60)
+        new_nameplates.add("3")
+        app.claim_nameplate("4", "side1", 1)
+        app.claim_nameplate("4", "side2", 60)
+        new_nameplates.add("4")
+        app.claim_nameplate("5", "side1", 60)
+        app.claim_nameplate("5", "side2", 61)
+        new_nameplates.add("5")
 
         rv.prune_all_apps(now=123, old=50)
 
@@ -344,6 +345,14 @@
                          db.execute("SELECT * FROM `mailboxes`").fetchall()}
         self.assertEqual(len(new_nameplates), len(mailboxes))
 
+        self.assertRaises(ValueError,
+                          app.claim_nameplate, "letters", "side1", 1)
+        long_but_ok = "1234"*10
+        app.claim_nameplate(long_but_ok, "side1", 1)
+        too_long = long_but_ok + "5"
+        self.assertRaises(ValueError,
+                          app.claim_nameplate, too_long, "side1", 1)
+
     def test_mailboxes(self):
         db = create_channel_db(":memory:")
         rv = make_server(db, blur_usage=3600)
@@ -404,7 +413,7 @@
 
         mbid = "mbid"
         if nameplate:
-            mbid = app.claim_nameplate("npid", "side1", when[mailbox])
+            mbid = app.claim_nameplate(npid, "side1", when[mailbox])
         mb = app.open_mailbox(mbid, "side1", when[mailbox])
 
         # the pruning algorithm doesn't care about the age of messages,
@@ -519,7 +528,7 @@
         rv = make_server(db, blur_usage=3600, usage_db=usage_db)
         APPID = "appid"
         app = rv.get_app(APPID)
-        app.claim_nameplate("npid", "side1", 10) # start time is 10
+        app.claim_nameplate(npid, "side1", 10) # start time is 10
         rv.prune_all_apps(now=123, old=50)
         # start time should be rounded to top of the hour (blur_usage=3600)
         row = usage_db.execute("SELECT * FROM `nameplates`").fetchone()
@@ -537,7 +546,7 @@
         rv = make_server(db, blur_usage=None, usage_db=usage_db)
         APPID = "appid"
         app = rv.get_app(APPID)
-        app.claim_nameplate("npid", "side1", 10) # start time is 10
+        app.claim_nameplate(npid, "side1", 10) # start time is 10
         rv.prune_all_apps(now=123, old=50)
         row = usage_db.execute("SELECT * FROM `nameplates`").fetchone()
         self.assertEqual(row["started"], 10)
@@ -615,17 +624,32 @@
     def test_welcome_error(self):
         db = create_channel_db(":memory:")
         s = make_server(db, signal_error="error!")
-        self.assertEqual(s.get_welcome(), {"error": "error!"})
+        self.assertEqual(
+            s.get_welcome(),
+            {
+                "error": "error!",
+            }
+        )
 
     def test_welcome_advertise_version(self):
         db = create_channel_db(":memory:")
         s = make_server(db, advertise_version="version")
-        self.assertEqual(s.get_welcome(), {"current_cli_version": "version"})
+        self.assertEqual(
+            s.get_welcome(),
+            {
+                "current_cli_version": "version",
+            }
+        )
 
     def test_welcome_message_of_the_day(self):
         db = create_channel_db(":memory:")
         s = make_server(db, welcome_motd="hello world")
-        self.assertEqual(s.get_welcome(), {"motd": "hello world"})
+        self.assertEqual(
+            s.get_welcome(),
+            {
+                "motd": "hello world",
+            }
+        )
 
 # exercise _find_available_nameplate_id failing
 # exercise CrowdedError
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_stats.py
 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_stats.py
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_stats.py
      2025-10-08 18:43:07.000000000 +0200
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_stats.py
      2026-05-14 21:00:08.000000000 +0200
@@ -3,6 +3,8 @@
 from ..database import create_channel_db, create_usage_db
 from ..server import make_server, CrowdedError
 
+np1 = "1"
+
 class _Make:
     def make(self, blur_usage=None, with_usage_db=True):
         self._cdb = create_channel_db(":memory:")
@@ -68,27 +70,27 @@
 class Nameplate(_Make, unittest.TestCase):
     def test_nameplate_happy(self):
         s, db, app = self.make()
-        app.claim_nameplate("n1", "s1", 1)
-        app.claim_nameplate("n1", "s2", 3)
-        app.release_nameplate("n1", "s1", 6)
+        app.claim_nameplate(np1, "s1", 1)
+        app.claim_nameplate(np1, "s2", 3)
+        app.release_nameplate(np1, "s1", 6)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [])
-        app.release_nameplate("n1", "s2", 10)
+        app.release_nameplate(np1, "s2", 10)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [dict(app_id="appid", result="happy",
                                started=1, waiting_time=2, total_time=9)])
 
     def test_nameplate_lonely(self):
         s, db, app = self.make()
-        app.claim_nameplate("n1", "s1", 1)
-        app.release_nameplate("n1", "s1", 6)
+        app.claim_nameplate(np1, "s1", 1)
+        app.release_nameplate(np1, "s1", 6)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [dict(app_id="appid", result="lonely",
                                started=1, waiting_time=None, total_time=5)])
 
     def test_nameplate_pruney(self):
         s, db, app = self.make()
-        app.claim_nameplate("n1", "s1", 1)
+        app.claim_nameplate(np1, "s1", 1)
         app.prune(10, 5) # prune at t=10, anything earlier than 5 is "old"
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [dict(app_id="appid", result="pruney",
@@ -96,16 +98,16 @@
 
     def test_nameplate_crowded(self):
         s, db, app = self.make()
-        app.claim_nameplate("n1", "s1", 1)
-        app.claim_nameplate("n1", "s2", 2)
+        app.claim_nameplate(np1, "s1", 1)
+        app.claim_nameplate(np1, "s2", 2)
         with self.assertRaises(CrowdedError):
-            app.claim_nameplate("n1", "s3", 3)
+            app.claim_nameplate(np1, "s3", 3)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [])
-        app.release_nameplate("n1", "s1", 4)
+        app.release_nameplate(np1, "s1", 4)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [])
-        app.release_nameplate("n1", "s2", 5)
+        app.release_nameplate(np1, "s2", 5)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [])
         #print(self._cdb.execute("SELECT * FROM `nameplates`").fetchall())
@@ -113,17 +115,17 @@
         # TODO: to get "crowded", we need all three sides to release the
         # nameplate, even though the third side threw CrowdedError and thus
         # probably doesn't think it has a claim
-        app.release_nameplate("n1", "s3", 6)
+        app.release_nameplate(np1, "s3", 6)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [dict(app_id="appid", result="crowded",
                                started=1, waiting_time=1, total_time=5)])
 
     def test_nameplate_crowded_pruned(self):
         s, db, app = self.make()
-        app.claim_nameplate("n1", "s1", 1)
-        app.claim_nameplate("n1", "s2", 2)
+        app.claim_nameplate(np1, "s1", 1)
+        app.claim_nameplate(np1, "s2", 2)
         with self.assertRaises(CrowdedError):
-            app.claim_nameplate("n1", "s3", 3)
+            app.claim_nameplate(np1, "s3", 3)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [])
         app.prune(10, 5)
@@ -133,18 +135,18 @@
 
     def test_no_db(self):
         s, db, app = self.make(with_usage_db=False)
-        app.claim_nameplate("n1", "s1", 1)
-        app.release_nameplate("n1", "s1", 6)
+        app.claim_nameplate(np1, "s1", 1)
+        app.release_nameplate(np1, "s1", 6)
         s.dump_stats(3, 1)
 
     def test_nameplate_happy_blur_usage(self):
         s, db, app = self.make(blur_usage=20)
-        app.claim_nameplate("n1", "s1", 21)
-        app.claim_nameplate("n1", "s2", 23)
-        app.release_nameplate("n1", "s1", 26)
+        app.claim_nameplate(np1, "s1", 21)
+        app.claim_nameplate(np1, "s2", 23)
+        app.release_nameplate(np1, "s1", 26)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [])
-        app.release_nameplate("n1", "s2", 30)
+        app.release_nameplate(np1, "s2", 30)
         self.assertEqual(db.execute("SELECT * FROM `nameplates`").fetchall(),
                          [dict(app_id="appid", result="happy",
                                started=20, waiting_time=2, total_time=9)])
@@ -152,8 +154,8 @@
 class Mailbox(_Make, unittest.TestCase):
     def test_mailbox_prune_quiet(self):
         s, db, app = self.make()
-        app.claim_nameplate("n1", "s1", 1)
-        app.release_nameplate("n1", "s1", 2)
+        app.claim_nameplate(np1, "s1", 1)
+        app.release_nameplate(np1, "s1", 2)
         app.prune(10, 5)
         self.assertEqual(db.execute("SELECT * FROM `mailboxes`").fetchall(),
                          [dict(app_id="appid", for_nameplate=1, 
result="pruney",
@@ -161,9 +163,9 @@
 
     def test_mailbox_lonely(self):
         s, db, app = self.make()
-        mid = app.claim_nameplate("n1", "s1", 1)
+        mid = app.claim_nameplate(np1, "s1", 1)
         mbox = app.open_mailbox(mid, "s1", 2)
-        app.release_nameplate("n1", "s1", 3)
+        app.release_nameplate(np1, "s1", 3)
         mbox.close("s1", "mood-ignored", 4)
         self.assertEqual(db.execute("SELECT * FROM `mailboxes`").fetchall(),
                          [dict(app_id="appid", for_nameplate=1, 
result="lonely",
@@ -171,9 +173,9 @@
 
     def test_mailbox_happy(self):
         s, db, app = self.make()
-        mid = app.claim_nameplate("n1", "s1", 1)
+        mid = app.claim_nameplate(np1, "s1", 1)
         mbox1 = app.open_mailbox(mid, "s1", 2)
-        app.release_nameplate("n1", "s1", 3)
+        app.release_nameplate(np1, "s1", 3)
         mbox2 = app.open_mailbox(mid, "s2", 4)
         mbox1.close("s1", "happy", 5)
         mbox2.close("s2", "happy", 6)
@@ -183,9 +185,9 @@
 
     def test_mailbox_happy_blur_usage(self):
         s, db, app = self.make(blur_usage=20)
-        mid = app.claim_nameplate("n1", "s1", 21)
+        mid = app.claim_nameplate(np1, "s1", 21)
         mbox1 = app.open_mailbox(mid, "s1", 22)
-        app.release_nameplate("n1", "s1", 23)
+        app.release_nameplate(np1, "s1", 23)
         mbox2 = app.open_mailbox(mid, "s2", 24)
         mbox1.close("s1", "happy", 25)
         mbox2.close("s2", "happy", 26)
@@ -198,9 +200,9 @@
         # connect, but then at least one side says they're lonely when they
         # close.
         s, db, app = self.make()
-        mid = app.claim_nameplate("n1", "s1", 1)
+        mid = app.claim_nameplate(np1, "s1", 1)
         mbox1 = app.open_mailbox(mid, "s1", 2)
-        app.release_nameplate("n1", "s1", 3)
+        app.release_nameplate(np1, "s1", 3)
         mbox2 = app.open_mailbox(mid, "s2", 4)
         mbox1.close("s1", "lonely", 5)
         mbox2.close("s2", "happy", 6)
@@ -210,9 +212,9 @@
 
     def test_mailbox_scary(self):
         s, db, app = self.make()
-        mid = app.claim_nameplate("n1", "s1", 1)
+        mid = app.claim_nameplate(np1, "s1", 1)
         mbox1 = app.open_mailbox(mid, "s1", 2)
-        app.release_nameplate("n1", "s1", 3)
+        app.release_nameplate(np1, "s1", 3)
         mbox2 = app.open_mailbox(mid, "s2", 4)
         mbox1.close("s1", "scary", 5)
         mbox2.close("s2", "happy", 6)
@@ -222,9 +224,9 @@
 
     def test_mailbox_errory(self):
         s, db, app = self.make()
-        mid = app.claim_nameplate("n1", "s1", 1)
+        mid = app.claim_nameplate(np1, "s1", 1)
         mbox1 = app.open_mailbox(mid, "s1", 2)
-        app.release_nameplate("n1", "s1", 3)
+        app.release_nameplate(np1, "s1", 3)
         mbox2 = app.open_mailbox(mid, "s2", 4)
         mbox1.close("s1", "errory", 5)
         mbox2.close("s2", "happy", 6)
@@ -234,9 +236,9 @@
 
     def test_mailbox_errory_scary(self):
         s, db, app = self.make()
-        mid = app.claim_nameplate("n1", "s1", 1)
+        mid = app.claim_nameplate(np1, "s1", 1)
         mbox1 = app.open_mailbox(mid, "s1", 2)
-        app.release_nameplate("n1", "s1", 3)
+        app.release_nameplate(np1, "s1", 3)
         mbox2 = app.open_mailbox(mid, "s2", 4)
         mbox1.close("s1", "errory", 5)
         mbox2.close("s2", "scary", 6)
@@ -246,9 +248,9 @@
 
     def test_mailbox_crowded(self):
         s, db, app = self.make()
-        mid = app.claim_nameplate("n1", "s1", 1)
+        mid = app.claim_nameplate(np1, "s1", 1)
         mbox1 = app.open_mailbox(mid, "s1", 2)
-        app.release_nameplate("n1", "s1", 3)
+        app.release_nameplate(np1, "s1", 3)
         mbox2 = app.open_mailbox(mid, "s2", 4)
         with self.assertRaises(CrowdedError):
             app.open_mailbox(mid, "s3", 5)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_web.py
 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_web.py
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_web.py
        2025-10-08 18:43:07.000000000 +0200
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_web.py
        2026-05-15 19:14:29.000000000 +0200
@@ -10,6 +10,9 @@
 from .common import ServerBase, _Util
 from .ws_client import WSFactory
 
+np1 = "1"
+np2 = "2"
+
 class WebSocketProtocolOptions(unittest.TestCase):
     @mock.patch('wormhole_mailbox_server.web.WebSocketServerFactory')
     def test_set(self, fake_factory):
@@ -62,7 +65,6 @@
             expected = "ws client connecting: tcp4:127.0.0.1:%d" % client_port
             self.assertEqual(l.mock_calls, [mock.call(expected)])
 
-
     @inlineCallbacks
     def test_no_log_http(self):
         yield self._setup_relay(do_listen=True, web_log_requests=False)
@@ -111,8 +113,7 @@
 
     def check_welcome(self, data):
         self.failUnlessIn("welcome", data)
-        self.assertEqual(data["welcome"],
-                             {"current_cli_version": "advertised.version"})
+        self.assertEqual(data["welcome"]["current_cli_version"], 
"advertised.version")
 
     @inlineCallbacks
     def test_welcome(self):
@@ -222,7 +223,7 @@
 
         app = self._server.get_app("appid")
         nameplate_id1 = app.allocate_nameplate("side", 0)
-        app.claim_nameplate("np2", "side", 0)
+        app.claim_nameplate(np2, "side", 0)
 
         c1.send("list")
         m = yield c1.next_non_ack()
@@ -232,7 +233,7 @@
             self.assertEqual(type(n), dict)
             self.assertEqual(list(n.keys()), ["id"])
             nids.add(n["id"])
-        self.assertEqual(nids, {nameplate_id1, "np2"})
+        self.assertEqual(nids, {nameplate_id1, np2})
 
     @inlineCallbacks
     def test_allocate(self):
@@ -279,21 +280,21 @@
         self.assertEqual(err["type"], "error")
         self.assertEqual(err["error"], "claim requires 'nameplate'")
 
-        c1.send("claim", nameplate="np1")
+        c1.send("claim", nameplate=np1)
         m = yield c1.next_non_ack()
         self.assertEqual(m["type"], "claimed")
         mailbox_id = m["mailbox"]
         self.assertEqual(type(mailbox_id), str)
 
-        c1.send("claim", nameplate="np1")
+        c1.send("claim", nameplate=np1)
         err = yield c1.next_non_ack()
         self.assertEqual(err["type"], "error", err)
         self.assertEqual(err["error"], "only one claim per connection")
 
         nids = app.get_nameplate_ids()
         self.assertEqual(len(nids), 1)
-        self.assertEqual("np1", list(nids)[0])
-        np_row, side_rows = self._nameplate(app, "np1")
+        self.assertEqual(np1, list(nids)[0])
+        np_row, side_rows = self._nameplate(app, np1)
         self.assertEqual(len(side_rows), 1)
         self.assertEqual(side_rows[0]["side"], "side")
 
@@ -310,11 +311,11 @@
         c1.send("bind", appid="appid", side="side")
         app = self._server.get_app("appid")
 
-        app.claim_nameplate("np1", "side1", 0)
-        app.claim_nameplate("np1", "side2", 0)
+        app.claim_nameplate(np1, "side1", 0)
+        app.claim_nameplate(np1, "side2", 0)
 
         # the third claim will signal crowding
-        c1.send("claim", nameplate="np1")
+        c1.send("claim", nameplate=np1)
         err = yield c1.next_non_ack()
         self.assertEqual(err["type"], "error")
         self.assertEqual(err["error"], "crowded")
@@ -326,7 +327,7 @@
         c1.send("bind", appid="appid", side="side")
         app = self._server.get_app("appid")
 
-        app.claim_nameplate("np1", "side2", 0)
+        app.claim_nameplate(np1, "side2", 0)
 
         c1.send("release") # didn't do claim first
         err = yield c1.next_non_ack()
@@ -334,14 +335,14 @@
         self.assertEqual(err["error"],
                          "release without nameplate must follow claim")
 
-        c1.send("claim", nameplate="np1")
+        c1.send("claim", nameplate=np1)
         yield c1.next_non_ack()
 
         c1.send("release")
         m = yield c1.next_non_ack()
         self.assertEqual(m["type"], "released", m)
 
-        np_row, side_rows = self._nameplate(app, "np1")
+        np_row, side_rows = self._nameplate(app, np1)
         claims = [(row["side"], row["claimed"]) for row in side_rows]
         self.assertIn(("side", False), claims)
         self.assertIn(("side2", True), claims)
@@ -357,10 +358,10 @@
         yield c1.next_non_ack()
         c1.send("bind", appid="appid", side="side")
 
-        c1.send("claim", nameplate="np1")
+        c1.send("claim", nameplate=np1)
         yield c1.next_non_ack()
 
-        c1.send("release", nameplate="np1")
+        c1.send("release", nameplate=np1)
         m = yield c1.next_non_ack()
         self.assertEqual(m["type"], "released", m)
 
@@ -370,7 +371,7 @@
         yield c1.next_non_ack()
         c1.send("bind", appid="appid", side="side")
 
-        c1.send("release", nameplate="np1") # didn't do claim first, ignored
+        c1.send("release", nameplate=np1) # didn't do claim first, ignored
         m = yield c1.next_non_ack()
         self.assertEqual(m["type"], "released", m)
 
@@ -380,10 +381,10 @@
         yield c1.next_non_ack()
         c1.send("bind", appid="appid", side="side")
 
-        c1.send("claim", nameplate="np1")
+        c1.send("claim", nameplate=np1)
         yield c1.next_non_ack()
 
-        c1.send("release", nameplate="np2") # mismatching nameplate
+        c1.send("release", nameplate=np2) # mismatching nameplate
         err = yield c1.next_non_ack()
         self.assertEqual(err["type"], "error")
         self.assertEqual(err["error"],
@@ -435,8 +436,8 @@
         c1.send("bind", appid="appid", side="side")
         app = self._server.get_app("appid")
 
-        mbid = app.claim_nameplate("np1", "side1", 0)
-        app.claim_nameplate("np1", "side2", 0)
+        mbid = app.claim_nameplate(np1, "side1", 0)
+        app.claim_nameplate(np1, "side2", 0)
 
         # the third open will signal crowding
         c1.send("open", mailbox=mbid)
@@ -550,8 +551,8 @@
         c1.send("bind", appid="appid", side="side")
         app = self._server.get_app("appid")
 
-        mbid = app.claim_nameplate("np1", "side1", 0)
-        app.claim_nameplate("np1", "side2", 0)
+        mbid = app.claim_nameplate(np1, "side1", 0)
+        app.claim_nameplate(np1, "side2", 0)
 
         # a close that allocates a third side will signal crowding
         c1.send("close", mailbox=mbid)
@@ -591,12 +592,12 @@
         c.send("bind", appid="appid", side="side")
         app = self._server.get_app("appid")
 
-        c.send("claim", nameplate="np1")
+        c.send("claim", nameplate=np1)
         m = yield c.next_non_ack()
         self.assertEqual(m["type"], "claimed")
         mailbox_id = m["mailbox"]
         self.assertEqual(type(mailbox_id), str)
-        np_row, side_rows = self._nameplate(app, "np1")
+        np_row, side_rows = self._nameplate(app, np1)
         claims = [(row["side"], row["claimed"]) for row in side_rows]
         self.assertEqual(claims, [("side", True)])
         c.close()
@@ -605,11 +606,11 @@
         c = yield self.make_client()
         yield c.next_non_ack()
         c.send("bind", appid="appid", side="side")
-        c.send("claim", nameplate="np1") # idempotent
+        c.send("claim", nameplate=np1) # idempotent
         m = yield c.next_non_ack()
         self.assertEqual(m["type"], "claimed")
         self.assertEqual(m["mailbox"], mailbox_id) # mailbox id is stable
-        np_row, side_rows = self._nameplate(app, "np1")
+        np_row, side_rows = self._nameplate(app, np1)
         claims = [(row["side"], row["claimed"]) for row in side_rows]
         self.assertEqual(claims, [("side", True)])
         c.close()
@@ -620,10 +621,10 @@
         c.send("bind", appid="appid", side="side")
         # we haven't done a claim with this particular connection, but we can
         # still send a release as long as we include the nameplate
-        c.send("release", nameplate="np1") # release-without-claim
+        c.send("release", nameplate=np1) # release-without-claim
         m = yield c.next_non_ack()
         self.assertEqual(m["type"], "released")
-        np_row, side_rows = self._nameplate(app, "np1")
+        np_row, side_rows = self._nameplate(app, np1)
         self.assertEqual(np_row, None)
         c.close()
         yield c.d
@@ -632,10 +633,10 @@
         yield c.next_non_ack()
         c.send("bind", appid="appid", side="side")
         # and the release is idempotent, when done on separate connections
-        c.send("release", nameplate="np1")
+        c.send("release", nameplate=np1)
         m = yield c.next_non_ack()
         self.assertEqual(m["type"], "released")
-        np_row, side_rows = self._nameplate(app, "np1")
+        np_row, side_rows = self._nameplate(app, np1)
         self.assertEqual(np_row, None)
         c.close()
         yield c.d
@@ -654,14 +655,14 @@
         c = yield self.make_client()
         yield c.next_non_ack()
         c.send("bind", appid="appid", side="side")
-        c.send("claim", nameplate="np2")
+        c.send("claim", nameplate=np2)
         m = yield c.next_non_ack()
         self.assertEqual(m["type"], "claimed")
-        app.claim_nameplate("np2", "side2", 0)
-        c.send("release", nameplate="np2")
+        app.claim_nameplate(np2, "side2", 0)
+        c.send("release", nameplate=np2)
         m = yield c.next_non_ack()
         self.assertEqual(m["type"], "released")
-        np_row, side_rows = self._nameplate(app, "np2")
+        np_row, side_rows = self._nameplate(app, np2)
         claims = sorted([(row["side"], row["claimed"]) for row in side_rows])
         self.assertEqual(claims, [("side", 0), ("side2", 1)])
         c.close()
@@ -670,12 +671,12 @@
         c = yield self.make_client()
         yield c.next_non_ack()
         c.send("bind", appid="appid", side="side")
-        c.send("claim", nameplate="np2") # new claim is forbidden
+        c.send("claim", nameplate=np2) # new claim is forbidden
         err = yield c.next_non_ack()
         self.assertEqual(err["type"], "error")
         self.assertEqual(err["error"], "reclaimed")
 
-        np_row, side_rows = self._nameplate(app, "np2")
+        np_row, side_rows = self._nameplate(app, np2)
         claims = sorted([(row["side"], row["claimed"]) for row in side_rows])
         self.assertEqual(claims, [("side", 0), ("side2", 1)])
         c.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_websocket.py
 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_websocket.py
--- 
old/magic_wormhole_mailbox_server-0.6.0/src/wormhole_mailbox_server/test/test_websocket.py
  1970-01-01 01:00:00.000000000 +0100
+++ 
new/magic_wormhole_mailbox_server-0.8.0/src/wormhole_mailbox_server/test/test_websocket.py
  2026-05-15 19:14:29.000000000 +0200
@@ -0,0 +1,182 @@
+import json
+from twisted.trial import unittest
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.address import IPv4Address
+from ..server_websocket import WebSocketServerFactory
+from autobahn.twisted.testing import create_pumper, create_memory_agent, 
MemoryReactorClock
+from autobahn.twisted.websocket import WebSocketClientProtocol
+
+
+class FakeServer:
+    """
+    Fake enough of the internal 'Server' object to appease the
+    WebSocket server.
+
+    """
+    def get_welcome(self):
+        return {
+            "motd": "fake message of the day"
+        }
+
+    def get_log_requests(self):
+        return False
+
+
+class WebSocket(unittest.TestCase):
+    """
+    Details of the server WebSocket protocol
+    """
+
+    def setUp(self):
+        self.pumper = create_pumper()
+        self.reactor = MemoryReactorClock()
+        return self.pumper.start()
+
+    def tearDown(self):
+        return self.pumper.stop()
+
+    def create_server_protocol(self):
+        """
+        Used by the Agent to create the in-memory transport server-side
+        WebSocket protocol (we actually create the 'real' protocol
+        objects since that is what we're testing here.)
+        """
+        factory = WebSocketServerFactory(
+            "ws://localhost:4000/v1",
+            FakeServer(),
+        )
+        addr = IPv4Address("TCP", "localhost", 4000)
+        return factory.buildProtocol(addr)
+
+    @inlineCallbacks
+    def test_server_version_string(self):
+        """
+        Our server version string from Autobahn should make sense
+        """
+        server_header = None
+        agent = create_memory_agent(
+            self.reactor,
+            self.pumper,
+            self.create_server_protocol
+        )
+
+        class FakeClient(WebSocketClientProtocol):
+            def onConnect(self, cr):
+                nonlocal server_header
+                server_header = cr.headers.get("server")
+
+        proto = yield agent.open("ws://localhost:4000/v1", dict(), FakeClient)
+        proto.sendClose()
+        yield proto.is_closed
+
+        assert "Magic Wormhole" in server_header, "Incorrect Server: header 
sent"
+
+    @inlineCallbacks
+    def test_reflected_address(self):
+        """
+        The Welcome message should include our address information
+        """
+        welcome = None
+        agent = create_memory_agent(
+            self.reactor,
+            self.pumper,
+            self.create_server_protocol
+        )
+
+        class FakeClient(WebSocketClientProtocol):
+            def onMessage(self, payload, isBinary):
+                js = json.loads(payload)
+                nonlocal welcome
+                if welcome is None:
+                    welcome = js.get("welcome", None)
+                return super().onMessage(payload, isBinary)
+
+        proto = yield agent.open("ws://localhost:4000/v1", dict(), FakeClient)
+        proto.sendClose()
+        yield proto.is_closed
+
+        assert welcome is not None, "Failed to receive Welcome message"
+        ya = welcome.get("your-address", None)
+        assert ya, "Expected 'your-address' in Welcome message"
+        assert ya["port"] == 31337
+        assert "ipv4" in ya or "ipv6" in ya, "Expected either IPv4 or IPv6 
address"
+
+    @inlineCallbacks
+    def test_reflected_caddy(self):
+        """
+        The Welcome message should include our address information
+        when sent via x-real-ip headers
+        """
+        welcome = None
+
+        headers = {
+            "x-real-ip": "127.1.2.3",
+            "x-real-port": "54321",
+        }
+
+        agent = create_memory_agent(
+            self.reactor,
+            self.pumper,
+            self.create_server_protocol,
+        )
+
+        class FakeClient(WebSocketClientProtocol):
+            def onMessage(self, payload, isBinary):
+                js = json.loads(payload)
+                nonlocal welcome
+                if welcome is None:
+                    welcome = js.get("welcome", None)
+                return super().onMessage(payload, isBinary)
+
+        proto = yield agent.open("ws://localhost:4000/v1", {"headers": 
headers}, FakeClient)
+        proto.sendClose()
+        yield proto.is_closed
+
+        assert welcome is not None, "Failed to receive Welcome message"
+        self.assertEqual(
+            welcome["your-address"],
+            {
+                "ipv4": "127.1.2.3",
+                "port": 54321,
+            }
+        )
+
+    @inlineCallbacks
+    def test_reflected_caddyv6(self):
+        """
+        The Welcome message should include our address information
+        when sent via x-real-ip headers (IPv6 version)
+        """
+        welcome = None
+
+        headers = {
+            "x-real-ip": "::1",
+            "x-real-port": "54321",
+        }
+
+        agent = create_memory_agent(
+            self.reactor,
+            self.pumper,
+            self.create_server_protocol,
+        )
+
+        class FakeClient(WebSocketClientProtocol):
+            def onMessage(self, payload, isBinary):
+                js = json.loads(payload)
+                nonlocal welcome
+                if welcome is None:
+                    welcome = js.get("welcome", None)
+                return super().onMessage(payload, isBinary)
+
+        proto = yield agent.open("ws://localhost:4000/v1", {"headers": 
headers}, FakeClient)
+        proto.sendClose()
+        yield proto.is_closed
+
+        assert welcome is not None, "Failed to receive Welcome message"
+        self.assertEqual(
+            welcome["your-address"],
+            {
+                "ipv6": "::1",
+                "port": 54321,
+            }
+        )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/magic_wormhole_mailbox_server-0.6.0/tox.ini 
new/magic_wormhole_mailbox_server-0.8.0/tox.ini
--- old/magic_wormhole_mailbox_server-0.6.0/tox.ini     2025-10-08 
18:43:07.000000000 +0200
+++ new/magic_wormhole_mailbox_server-0.8.0/tox.ini     2026-05-14 
21:00:08.000000000 +0200
@@ -4,7 +4,7 @@
 # and then run "tox" from this directory.
 
 [tox]
-envlist = {py37,py38,py39,py310,py311,pypy}
+envlist = {py310,py311,py312,py313,py314,pypy}
 skip_missing_interpreters = True
 minversion = 2.4.0
 

Reply via email to