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 src.SantaExchange.Message import Message
from src.SantaExchange.MessageHandler import MessageHandler
from src.SantaExchange.SantasBrain import Brain
from src.SantaExchange.UserInterface import UserInterface
from SantaExchange.Message import Message
from SantaExchange.MessageHandler import MessageHandler
from SantaExchange.SantasBrain import Brain
from SantaExchange.UserInterface import UserInterface
class ExchangeClient(MessageHandler, UserInterface):
def __init__(self, send_message_function, receive_user_function, announce_recipient_function):

View File

@ -1,23 +1,56 @@
import "./pyodide.js";
async function prepare() {
self.postMessage({type: 'status', stage: 'initialize', text: 'Initializing...'});
let pyodide = await loadPyodide();
await pyodide.loadPackage("pycryptodome");
await pyodide.loadPackage("./santaexchange-0.1-py3-none-any.whl")
pyodide.runPython(`
class Test:
def calculate(self):
return 15
await pyodide.loadPackage("./santaexchange-0.1-py3-none-any.whl");
let response = await fetch('./exchange_client.py');
pyodide.runPython(await response.text())
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');
console.log(pyodide.globals.get('a').calculate());
addEventListener('message', e => {
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();
addEventListener('message', e => {
self.postMessage('Loading pyodide');
self.postMessage('Loaded pyodide');
});

View File

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

View File

@ -12,38 +12,70 @@
<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 />
<button id="submit_info_button">Submit your info</button>
<button id="submit_info_button" onclick="submitInfo();">Submit your info</button>
<br /><br /><br />
<p id="statusParagraph"></p>
<button>Start with current participants</button>
<pre id="statusParagraph"></pre>
<button id="start_exchange_button" onclick="submitStart();">Start with current participants</button>
<p>Current participants:</p>
<div id='participantList'></div>
<!--<script type="text/javascript">
<ul id='participantList'>
</ul>
<script type="text/javascript">
const socket = io();
const client_id = Math.floor(Math.random() * 2**64)
socket.emit('join', {room: window.location.pathname, 'client_id': client_id})
socket.on('message', data => {
// All messages are broadcast to everyone, so discard messages that were actually sent from ourselves
data = JSON.parse(data);
if (data['client_id'] != client_id) {
console.log(data);
console.log(data['message']);
window.pass_message_to_python(JSON.stringify(data['message']));
}
});
</script>-->
const other_users = []
let submitButton = document.getElementById('submit_info_button');
let startButton = document.getElementById('start_exchange_button');
submitButton.disabled = true;
startButton.disabled = true;
<script type="text/javascript">
const exchangeWorker = new Worker("{{ url_for('static', filename='exchange_worker.js') }}", { type: 'module' });
exchangeWorker.onmessage = (e) => {
console.log('Received message:');
console.log(e);
document.getElementById('statusParagraph').innerHTML = e.data;
if (e.data.type == 'status') {
document.getElementById('statusParagraph').innerHTML = e.data.text;
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>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import base64
from src.SantaExchange.Message import Message
from SantaExchange.Message import 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.ECC import EccKey
from src.SantaExchange import Message
from src.SantaExchange.Crypto.CSPRNG import CSPRNG
from src.SantaExchange.Crypto.CommutativeCipher import CommutativeCipher
from src.SantaExchange.MessageHandler import MessageHandler
from src.SantaExchange.MessageTypes.Announcement import AnnouncementMessage
from src.SantaExchange.MessageTypes.Introduction import IntroductionMessage
from src.SantaExchange.MessageTypes.Ready import ReadyMessage
from src.SantaExchange.MessageTypes.Shuffle import ShuffleMessage
from src.SantaExchange.UserInterface import UserInterface
from SantaExchange import Message
from SantaExchange.Crypto.CSPRNG import CSPRNG
from SantaExchange.Crypto.CommutativeCipher import CommutativeCipher
from SantaExchange.MessageHandler import MessageHandler
from SantaExchange.MessageTypes.Announcement import AnnouncementMessage
from SantaExchange.MessageTypes.Introduction import IntroductionMessage
from SantaExchange.MessageTypes.Ready import ReadyMessage
from SantaExchange.MessageTypes.Shuffle import ShuffleMessage
from SantaExchange.UserInterface import UserInterface
logger = logging.getLogger(__name__)
@ -129,6 +129,10 @@ class Brain:
self.chosen_participants = sorted([name for name, key in confirmed_existing])
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):
# If this is an introduction message, it needs special handling
if isinstance(message, IntroductionMessage):
@ -149,14 +153,13 @@ class Brain:
self.received_messages[name].append(message)
# Each received message triggers the main function of this class
messages_to_send = self.santa_loop()
for message in messages_to_send:
self.message_handler.send_message(message, self.signing_key)
self.santa_loop_and_send()
def receive_introduction_message(self, message: IntroductionMessage):
discovered_new_participant = False
name = message.get_name()
key = message.get_key()
logger.debug(f'Receiving introduction from user {name}')
with self.thread_lock:
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
if (
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
):
self.user_interface.receive_user(name)
logger.debug('Sending own introduction message')
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.
# This is probably inefficient, but it makes it easier to follow what the code is doing
def santa_loop(self) -> list[Message]:
@ -203,6 +216,7 @@ class Brain:
or self.announcement_build_cipher is None
or self.announcement_shuffle_cipher is None
):
logger.debug('Building ciphers')
should_continue = self.build_ciphers()
if not should_continue:
return messages_to_send
@ -212,6 +226,7 @@ class Brain:
# Shuffle cards
if not self.sent_card_shuffling:
logger.debug('Shuffling vards')
shuffle_message = self.build_shuffle_message(all_participants)
if shuffle_message is None:
return messages_to_send
@ -220,6 +235,7 @@ class Brain:
# Decrypt shuffled cards
if not self.sent_card_decryption:
logger.debug('Decrypting shuffled cards')
decrypt_cards_message = self.build_decrypt_cards_message(all_participants)
if decrypt_cards_message is None:
return messages_to_send
@ -432,6 +448,7 @@ class Brain:
return ShuffleMessage(self.own_name, decrypted_cards, DECRYPT_CARDS_STAGE)
def decrypt_card_value(self, card: bytes):
logger.debug('Decrypting own drawn card')
decrypted_card_bytes = self.card_exchange_cipher.decode(card)
if decrypted_card_bytes not in self.card_values:
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.start_listeners: list[Callable[[list[str]], None]] = []
# Must be implemented by child class
def receive_user(self, name: str):
pass
raise NotImplementedError
# Must be implemented by child class
def announce_recipient(self, name: str, other_info: str):
pass
raise NotImplementedError
def set_user_info(self, name: str, info_for_santa: str):
for listener in self.user_info_listeners: