Web exchange seems to work now

This commit is contained in:
Martin Asprusten 2025-04-20 14:13:29 +02:00
parent 14cdefea69
commit 564e449e33
11 changed files with 155 additions and 69 deletions

View File

@ -1,10 +1,11 @@
from typing import Optional, Callable
from Crypto.PublicKey.ECC import EccKey from Crypto.PublicKey.ECC import EccKey
from src.SantaExchange.Message import Message from SantaExchange.Message import Message
from src.SantaExchange.MessageHandler import MessageHandler from SantaExchange.MessageHandler import MessageHandler
from src.SantaExchange.SantasBrain import Brain from SantaExchange.SantasBrain import Brain
from src.SantaExchange.UserInterface import UserInterface from SantaExchange.UserInterface import UserInterface
class ExchangeClient(MessageHandler, UserInterface): class ExchangeClient(MessageHandler, UserInterface):
def __init__(self, send_message_function, receive_user_function, announce_recipient_function): def __init__(self, send_message_function, receive_user_function, announce_recipient_function):

View File

@ -1,23 +1,56 @@
import "./pyodide.js"; import "./pyodide.js";
async function prepare() { async function prepare() {
self.postMessage({type: 'status', stage: 'initialize', text: 'Initializing...'});
let pyodide = await loadPyodide(); let pyodide = await loadPyodide();
await pyodide.loadPackage("pycryptodome"); await pyodide.loadPackage("pycryptodome");
await pyodide.loadPackage("./santaexchange-0.1-py3-none-any.whl") await pyodide.loadPackage("./santaexchange-0.1-py3-none-any.whl");
pyodide.runPython(` let response = await fetch('./exchange_client.py');
class Test: pyodide.runPython(await response.text())
def calculate(self):
return 15
a = Test() function sendMessage(messageString) {
self.postMessage({type: 'message', message: messageString})
}
function receiveUser(username) {
self.postMessage({type: 'new_user', username: username})
}
function announceRecipient(recipient_name, recipient_info) {
self.postMessage({type: 'status', stage: 'finished', text: `Your secret santa receiver is \n${recipient_name}\n${recipient_info}`})
}
function printLogs(record) {
console.log(record);
}
pyodide.globals.set('send_message', sendMessage);
pyodide.globals.set('receive_user', receiveUser);
pyodide.globals.set('announce_recipient', announceRecipient);
pyodide.globals.set('print_logs_function', printLogs);
pyodide.runPython(`
exchange_worker = ExchangeClient(send_message, receive_user, announce_recipient)
`); `);
console.log('Running python function'); addEventListener('message', e => {
console.log(pyodide.globals.get('a').calculate()); if (e.data.type == 'message') {
pyodide.globals.get('exchange_worker').decode_received_message(e.data.message);
} else if (e.data.type == 'set_user') {
let username = e.data.username;
let userinfo = e.data.userinfo;
pyodide.globals.get('exchange_worker').set_user_info(username, userinfo);
postMessage({type: 'status', stage: 'wait_for_start', text: 'Waiting for other participants'});
} else if (e.data.type == 'start') {
postMessage({
type: 'status',
stage: 'exchanging',
text: 'Performing exchange when all users have pressed start. This may take a minute or so.'
});
pyodide.globals.get('exchange_worker').start_exchange(e.data.users);
}
});
self.postMessage({type: 'status', stage: 'wait_for_user', text: 'Initialized'})
} }
prepare(); prepare();
addEventListener('message', e => {
self.postMessage('Loading pyodide');
self.postMessage('Loaded pyodide');
});

View File

@ -35,4 +35,5 @@ input {
textarea { textarea {
width: 30vw; width: 30vw;
height: 20vh; height: 20vh;
font-size: 20px;
} }

View File

@ -12,38 +12,70 @@
<br /> <br />
<label for="info_for_santa">Info for your santa (e.g. shipping address or similar):</label><br /> <label for="info_for_santa">Info for your santa (e.g. shipping address or similar):</label><br />
<textarea id="info_for_santa"></textarea><br /> <textarea id="info_for_santa"></textarea><br />
<button id="submit_info_button">Submit your info</button> <button id="submit_info_button" onclick="submitInfo();">Submit your info</button>
<br /><br /><br /> <br /><br /><br />
<p id="statusParagraph"></p> <pre id="statusParagraph"></pre>
<button>Start with current participants</button> <button id="start_exchange_button" onclick="submitStart();">Start with current participants</button>
<p>Current participants:</p> <p>Current participants:</p>
<div id='participantList'></div> <ul id='participantList'>
<!--<script type="text/javascript">
</ul>
<script type="text/javascript">
const socket = io(); const socket = io();
const client_id = Math.floor(Math.random() * 2**64) const client_id = Math.floor(Math.random() * 2**64)
socket.emit('join', {room: window.location.pathname, 'client_id': client_id})
socket.on('message', data => { const other_users = []
// All messages are broadcast to everyone, so discard messages that were actually sent from ourselves
data = JSON.parse(data); let submitButton = document.getElementById('submit_info_button');
if (data['client_id'] != client_id) { let startButton = document.getElementById('start_exchange_button');
console.log(data);
console.log(data['message']); submitButton.disabled = true;
window.pass_message_to_python(JSON.stringify(data['message'])); startButton.disabled = true;
}
});
</script>-->
<script type="text/javascript">
const exchangeWorker = new Worker("{{ url_for('static', filename='exchange_worker.js') }}", { type: 'module' }); const exchangeWorker = new Worker("{{ url_for('static', filename='exchange_worker.js') }}", { type: 'module' });
exchangeWorker.onmessage = (e) => { exchangeWorker.onmessage = (e) => {
console.log('Received message:'); if (e.data.type == 'status') {
console.log(e); document.getElementById('statusParagraph').innerHTML = e.data.text;
document.getElementById('statusParagraph').innerHTML = e.data; if (e.data.stage == 'wait_for_user') {
submitButton.disabled = false;
startButton.disabled = true;
} else if (e.data.stage == 'wait_for_start') {
submitButton.disabled = true;
startButton.disabled = false;
} else {
submitButton.disabled = true;
startButton.disabled = true;
}
} else if (e.data.type == 'message') {
socket.emit('message', {client_id: client_id, room: window.location.pathname, message: e.data.message});
} else if (e.data.type == 'new_user') {
let list = document.getElementById('participantList');
const childNode = document.createElement("li");
childNode.innerHTML = e.data.username;
other_users.push(e.data.username);
list.appendChild(childNode);
}
}; };
exchangeWorker.postMessage('Nothing'); socket.on('message', data => {
// All messages are broadcast to everyone, so discard messages that were actually sent from ourselves
if (data['client_id'] != client_id) {
exchangeWorker.postMessage({type: 'message', message: data['message']})
}
});
function submitInfo() {
let name = document.getElementById('own_name').value;
let info = document.getElementById('info_for_santa').value;
exchangeWorker.postMessage({type: 'set_user', username: name, userinfo: info});
}
function submitStart() {
exchangeWorker.postMessage({type: 'start', users: other_users});
}
socket.emit('join', {room: window.location.pathname, client_id: client_id})
</script> </script>
</body> </body>
</html> </html>

View File

@ -8,11 +8,11 @@ from json import JSONDecodeError
from Crypto.PublicKey import ECC from Crypto.PublicKey import ECC
from Crypto.PublicKey.ECC import EccKey from Crypto.PublicKey.ECC import EccKey
from src.SantaExchange.Message import Message from SantaExchange.Message import Message
from src.SantaExchange.MessageTypes.Announcement import AnnouncementMessage from SantaExchange.MessageTypes.Announcement import AnnouncementMessage
from src.SantaExchange.MessageTypes.Introduction import IntroductionMessage from SantaExchange.MessageTypes.Introduction import IntroductionMessage
from src.SantaExchange.MessageTypes.Ready import ReadyMessage from SantaExchange.MessageTypes.Ready import ReadyMessage
from src.SantaExchange.MessageTypes.Shuffle import ShuffleMessage from SantaExchange.MessageTypes.Shuffle import ShuffleMessage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,9 +21,9 @@ class MessageHandler:
def __init__(self): def __init__(self):
self.receivers: list[Callable[[Message], None]] = [] self.receivers: list[Callable[[Message], None]] = []
# Must be implemented by child class
def send_message(self, message: Message, signing_key: EccKey): def send_message(self, message: Message, signing_key: EccKey):
# Must be implemented by child SantaExchange raise NotImplementedError
pass
def add_message_receiver(self, message_receiver: Callable[[Message], None]): def add_message_receiver(self, message_receiver: Callable[[Message], None]):
self.receivers.append(message_receiver) self.receivers.append(message_receiver)

View File

@ -1,6 +1,6 @@
import base64 import base64
from src.SantaExchange.Message import Message from SantaExchange.Message import Message
class AnnouncementMessage(Message): class AnnouncementMessage(Message):

View File

@ -3,7 +3,7 @@ import base64
from Crypto.PublicKey import ECC from Crypto.PublicKey import ECC
from Crypto.PublicKey.ECC import EccKey from Crypto.PublicKey.ECC import EccKey
from src.SantaExchange.Message import Message from SantaExchange.Message import Message
class IntroductionMessage(Message): class IntroductionMessage(Message):

View File

@ -3,7 +3,7 @@ import base64
from Crypto.PublicKey import ECC from Crypto.PublicKey import ECC
from Crypto.PublicKey.ECC import EccKey from Crypto.PublicKey.ECC import EccKey
from src.SantaExchange.Message import Message from SantaExchange.Message import Message
class ReadyMessage(Message): class ReadyMessage(Message):

View File

@ -1,6 +1,6 @@
import base64 import base64
from src.SantaExchange.Message import Message from SantaExchange.Message import Message
class ShuffleMessage(Message): class ShuffleMessage(Message):

View File

@ -14,15 +14,15 @@ from Crypto.Protocol.DH import key_agreement
from Crypto.PublicKey import ECC from Crypto.PublicKey import ECC
from Crypto.PublicKey.ECC import EccKey from Crypto.PublicKey.ECC import EccKey
from src.SantaExchange import Message from SantaExchange import Message
from src.SantaExchange.Crypto.CSPRNG import CSPRNG from SantaExchange.Crypto.CSPRNG import CSPRNG
from src.SantaExchange.Crypto.CommutativeCipher import CommutativeCipher from SantaExchange.Crypto.CommutativeCipher import CommutativeCipher
from src.SantaExchange.MessageHandler import MessageHandler from SantaExchange.MessageHandler import MessageHandler
from src.SantaExchange.MessageTypes.Announcement import AnnouncementMessage from SantaExchange.MessageTypes.Announcement import AnnouncementMessage
from src.SantaExchange.MessageTypes.Introduction import IntroductionMessage from SantaExchange.MessageTypes.Introduction import IntroductionMessage
from src.SantaExchange.MessageTypes.Ready import ReadyMessage from SantaExchange.MessageTypes.Ready import ReadyMessage
from src.SantaExchange.MessageTypes.Shuffle import ShuffleMessage from SantaExchange.MessageTypes.Shuffle import ShuffleMessage
from src.SantaExchange.UserInterface import UserInterface from SantaExchange.UserInterface import UserInterface
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -129,6 +129,10 @@ class Brain:
self.chosen_participants = sorted([name for name, key in confirmed_existing]) self.chosen_participants = sorted([name for name, key in confirmed_existing])
self.message_handler.send_message(ready_message, self.signing_key) self.message_handler.send_message(ready_message, self.signing_key)
# Might as well start building ciphers and preparing immediately. Also, if the first user in the list happens
# to be the last person to send the start message, something needs to trigger the santa loop
self.santa_loop_and_send()
def receive_message(self, message: Message): def receive_message(self, message: Message):
# If this is an introduction message, it needs special handling # If this is an introduction message, it needs special handling
if isinstance(message, IntroductionMessage): if isinstance(message, IntroductionMessage):
@ -149,14 +153,13 @@ class Brain:
self.received_messages[name].append(message) self.received_messages[name].append(message)
# Each received message triggers the main function of this class # Each received message triggers the main function of this class
messages_to_send = self.santa_loop() self.santa_loop_and_send()
for message in messages_to_send:
self.message_handler.send_message(message, self.signing_key)
def receive_introduction_message(self, message: IntroductionMessage): def receive_introduction_message(self, message: IntroductionMessage):
discovered_new_participant = False discovered_new_participant = False
name = message.get_name() name = message.get_name()
key = message.get_key() key = message.get_key()
logger.debug(f'Receiving introduction from user {name}')
with self.thread_lock: with self.thread_lock:
if name in self.known_participants and self.known_participants[name][0] != key: if name in self.known_participants and self.known_participants[name][0] != key:
@ -174,12 +177,22 @@ class Brain:
# Send an introduction message, if we are ready for that # Send an introduction message, if we are ready for that
if ( if (
discovered_new_participant discovered_new_participant
and self.introduction_message is not None ):
logger.debug(f'Received new user {name}')
self.user_interface.receive_user(name)
if (
self.introduction_message is not None
and self.signing_key is not None and self.signing_key is not None
): ):
self.user_interface.receive_user(name) logger.debug('Sending own introduction message')
self.message_handler.send_message(self.introduction_message, self.signing_key) self.message_handler.send_message(self.introduction_message, self.signing_key)
def santa_loop_and_send(self):
messages_to_send = self.santa_loop()
for message in messages_to_send:
self.message_handler.send_message(message, self.signing_key)
# Santa's brain will be driven by receiving messages. We'll call this main method each time we receive a message. # Santa's brain will be driven by receiving messages. We'll call this main method each time we receive a message.
# This is probably inefficient, but it makes it easier to follow what the code is doing # This is probably inefficient, but it makes it easier to follow what the code is doing
def santa_loop(self) -> list[Message]: def santa_loop(self) -> list[Message]:
@ -203,6 +216,7 @@ class Brain:
or self.announcement_build_cipher is None or self.announcement_build_cipher is None
or self.announcement_shuffle_cipher is None or self.announcement_shuffle_cipher is None
): ):
logger.debug('Building ciphers')
should_continue = self.build_ciphers() should_continue = self.build_ciphers()
if not should_continue: if not should_continue:
return messages_to_send return messages_to_send
@ -212,6 +226,7 @@ class Brain:
# Shuffle cards # Shuffle cards
if not self.sent_card_shuffling: if not self.sent_card_shuffling:
logger.debug('Shuffling vards')
shuffle_message = self.build_shuffle_message(all_participants) shuffle_message = self.build_shuffle_message(all_participants)
if shuffle_message is None: if shuffle_message is None:
return messages_to_send return messages_to_send
@ -220,6 +235,7 @@ class Brain:
# Decrypt shuffled cards # Decrypt shuffled cards
if not self.sent_card_decryption: if not self.sent_card_decryption:
logger.debug('Decrypting shuffled cards')
decrypt_cards_message = self.build_decrypt_cards_message(all_participants) decrypt_cards_message = self.build_decrypt_cards_message(all_participants)
if decrypt_cards_message is None: if decrypt_cards_message is None:
return messages_to_send return messages_to_send
@ -432,6 +448,7 @@ class Brain:
return ShuffleMessage(self.own_name, decrypted_cards, DECRYPT_CARDS_STAGE) return ShuffleMessage(self.own_name, decrypted_cards, DECRYPT_CARDS_STAGE)
def decrypt_card_value(self, card: bytes): def decrypt_card_value(self, card: bytes):
logger.debug('Decrypting own drawn card')
decrypted_card_bytes = self.card_exchange_cipher.decode(card) decrypted_card_bytes = self.card_exchange_cipher.decode(card)
if decrypted_card_bytes not in self.card_values: if decrypted_card_bytes not in self.card_values:
logging.critical(f'Received an invalid card after shuffling. Secret santa exchange failed!') logging.critical(f'Received an invalid card after shuffling. Secret santa exchange failed!')

View File

@ -6,11 +6,13 @@ class UserInterface:
self.user_info_listeners: list[Callable[[str, str], None]] = [] self.user_info_listeners: list[Callable[[str, str], None]] = []
self.start_listeners: list[Callable[[list[str]], None]] = [] self.start_listeners: list[Callable[[list[str]], None]] = []
# Must be implemented by child class
def receive_user(self, name: str): def receive_user(self, name: str):
pass raise NotImplementedError
# Must be implemented by child class
def announce_recipient(self, name: str, other_info: str): def announce_recipient(self, name: str, other_info: str):
pass raise NotImplementedError
def set_user_info(self, name: str, info_for_santa: str): def set_user_info(self, name: str, info_for_santa: str):
for listener in self.user_info_listeners: for listener in self.user_info_listeners: