From fa7ab5d3ed02459a1a82e03adeb700bde17fa5dc Mon Sep 17 00:00:00 2001 From: n9k Date: Fri, 25 Feb 2022 23:44:54 +0000 Subject: [PATCH] Send new captcha over websocket with js --- anonstream/static/anonstream.js | 32 ++++++++++++++- anonstream/static/style.css | 3 ++ anonstream/utils/websocket.py | 30 +++++++++----- anonstream/websocket.py | 70 +++++++++++++++++++++------------ 4 files changed, 99 insertions(+), 36 deletions(-) diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js index 12769ea..68f8e91 100644 --- a/anonstream/static/anonstream.js +++ b/anonstream/static/anonstream.js @@ -296,13 +296,31 @@ const chat_form_captcha_digest = document.getElementById("chat-form_js__captcha- const chat_form_captcha_image = document.getElementById("chat-form_js__captcha-image"); const chat_form_captcha_answer = document.getElementById("chat-form_js__captcha-answer"); chat_form_captcha_image.addEventListener("loadstart", (event) => { + chat_form_captcha_image.removeAttribute("title"); + chat_form_captcha_image.removeAttribute("data-reloadable"); chat_form_captcha_image.alt = "Loading..."; }); chat_form_captcha_image.addEventListener("load", (event) => { chat_form_captcha_image.removeAttribute("alt"); + chat_form_captcha_image.dataset.reloadable = ""; + chat_form_captcha_image.title = "Click for a new captcha"; }); chat_form_captcha_image.addEventListener("error", (event) => { chat_form_captcha_image.alt = "Captcha failed to load"; + chat_form_captcha_image.dataset.reloadable = ""; + chat_form_captcha_image.title = "Click for a new captcha"; +}); +chat_form_captcha_image.addEventListener("click", (event) => { + if (chat_form_captcha_image.dataset.reloadable === undefined) { + return; + } + chat_form_submit.disabled = true; + chat_form_captcha_image.alt = "Waiting..."; + chat_form_captcha_image.removeAttribute("title"); + chat_form_captcha_image.removeAttribute("data-reloadable"); + chat_form_captcha_image.removeAttribute("src"); + const payload = {type: "captcha"}; + ws.send(JSON.stringify(payload)); }); const enable_captcha = (digest) => { chat_form_captcha_digest.value = digest; @@ -313,6 +331,7 @@ const enable_captcha = (digest) => { chat_form_comment.required = false; chat_form_captcha_image.removeAttribute("src"); chat_form_captcha_image.src = `/captcha.jpg?token=${encodeURIComponent(token)}&digest=${encodeURIComponent(digest)}`; + chat_form_submit.disabled = false; chat_form.dataset.captcha = ""; } const disable_captcha = () => { @@ -323,6 +342,7 @@ const disable_captcha = () => { chat_form_captcha_digest.value = ""; chat_form_captcha_answer.value = ""; chat_form_captcha_answer.required = false; + chat_form_submit.disabled = false; chat_form_captcha_image.removeAttribute("alt"); chat_form_captcha_image.removeAttribute("src"); } @@ -373,6 +393,7 @@ const on_websocket_message = (event) => { switch (receipt.type) { case "error": console.log("ws error", receipt); + chat_form_submit.disabled = false; break; case "init": @@ -394,6 +415,9 @@ const on_websocket_message = (event) => { // chat form captcha digest receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest); + // chat form submit button + chat_form_submit.disabled = false; + // remove messages the server isn't acknowledging the existance of const seqs = new Set(receipt.messages.map((message) => {return message.seq;})); const to_delete = []; @@ -486,6 +510,11 @@ const on_websocket_message = (event) => { update_user_tripcodes(); break; + case "captcha": + console.log("ws captcha", receipt); + receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest); + break; + default: console.log("incomprehensible websocket message", receipt); } @@ -547,7 +576,8 @@ const chat_form_comment = document.getElementById("chat-form_js__comment"); const chat_form_submit = document.getElementById("chat-form_js__submit"); chat_form.addEventListener("submit", (event) => { event.preventDefault(); - const payload = Object.fromEntries(new FormData(chat_form)); + const form = Object.fromEntries(new FormData(chat_form)); + const payload = {type: "message", form: form}; chat_form_submit.disabled = true; ws.send(JSON.stringify(payload)); }); diff --git a/anonstream/static/style.css b/anonstream/static/style.css index 07bf2fe..cb32653 100644 --- a/anonstream/static/style.css +++ b/anonstream/static/style.css @@ -187,6 +187,9 @@ noscript { color: inherit; font-size: 8pt; } +#chat-form_js__captcha-image[data-reloadable] { + cursor: pointer; +} #chat-form_js__captcha-answer { width: 8ch; } diff --git a/anonstream/utils/websocket.py b/anonstream/utils/websocket.py index 5f4cdb5..1dcaec0 100644 --- a/anonstream/utils/websocket.py +++ b/anonstream/utils/websocket.py @@ -1,19 +1,31 @@ class Malformed(Exception): pass +def get(t, pairs, key, default=None): + value = pairs.get(key, default) + if isinstance(value, t): + return value + else: + raise Malformed(f'malformed {key}') + def parse_websocket_data(receipt): if not isinstance(receipt, dict): raise Malformed('not a json object') - comment = receipt.get('comment') - if not isinstance(comment, str): - raise Malformed('malformed comment') + match receipt.get('type'): + case 'message': + form = get(dict, receipt, 'form') + nonce = get(str, form, 'nonce') + comment = get(str, form, 'comment') + digest = get(str, form, 'captcha-digest', '') + answer = get(str, form, 'captcha-answer', '') + return nonce, comment, digest, answer - nonce = receipt.get('nonce') - if not isinstance(nonce, str): - raise Malformed('malformed nonce') + case 'appearance': + raise NotImplemented - digest = receipt.get('captcha-digest', '') - answer = receipt.get('captcha-answer', '') + case 'captcha': + return None - return nonce, comment, digest, answer + case _: + raise Malformed('malformed type') diff --git a/anonstream/websocket.py b/anonstream/websocket.py index 3b340f7..9e9c34b 100644 --- a/anonstream/websocket.py +++ b/anonstream/websocket.py @@ -44,7 +44,7 @@ async def websocket_inbound(queue, user): finally: see(user) try: - nonce, comment, digest, answer = parse_websocket_data(receipt) + parsed = parse_websocket_data(receipt) except Malformed as e: error , *_ = e.args payload = { @@ -52,29 +52,47 @@ async def websocket_inbound(queue, user): 'because': error, } else: - try: - verification_happened = verify(user, digest, answer) - except BadCaptcha as e: - notice, *_ = e.args - else: - try: - message_was_added = add_chat_message( - user, - nonce, - comment, - ignore_empty=verification_happened, - ) - except Rejected as e: - notice, *_ = e.args - else: - deverify(user) - notice = None - payload = { - 'type': 'ack', - 'nonce': nonce, - 'next': generate_nonce(), - 'notice': notice, - 'clear': message_was_added, - 'digest': get_random_captcha_digest_for(user), - } + match parsed: + case [nonce, comment, digest, answer]: + payload = handle_inbound_message(user, *parsed) + + case None: + payload = handle_inbound_captcha(user) + queue.put_nowait(payload) + +def handle_inbound_captcha(user): + return { + 'type': 'captcha', + 'digest': get_random_captcha_digest_for(user), + } + +def handle_inbound_message(user, nonce, comment, digest, answer): + try: + verification_happened = verify(user, digest, answer) + except BadCaptcha as e: + notice, *_ = e.args + message_was_added = False + else: + try: + message_was_added = add_chat_message( + user, + nonce, + comment, + ignore_empty=verification_happened, + ) + except Rejected as e: + notice, *_ = e.args + message_was_added = False + else: + notice = None + if message_was_added: + deverify(user) + return { + 'type': 'ack', + 'nonce': nonce, + 'next': generate_nonce(), + 'notice': notice, + 'clear': message_was_added, + 'digest': get_random_captcha_digest_for(user), + }