From 2f4a9739c0117213c826c2c287a25c2c6ea9097f Mon Sep 17 00:00:00 2001 From: n9k Date: Mon, 21 Feb 2022 02:05:19 +0000 Subject: [PATCH] Show and enforce the captcha in js Also clear the chat form comment input only if the message was accepted. --- anonstream/chat.py | 4 +- anonstream/routes/nojs.py | 2 +- anonstream/static/anonstream.js | 71 ++++++++++++++++++------ anonstream/static/style.css | 98 ++++++++++++++++++++++----------- anonstream/utils/websocket.py | 4 +- anonstream/websocket.py | 33 ++++++----- 6 files changed, 140 insertions(+), 72 deletions(-) diff --git a/anonstream/chat.py b/anonstream/chat.py index b9bedb4..f519a6b 100644 --- a/anonstream/chat.py +++ b/anonstream/chat.py @@ -28,7 +28,7 @@ def get_all_messages_for_websocket(): def add_chat_message(user, nonce, comment, ignore_empty=False): # Special case: if the comment is empty, do nothing and return if ignore_empty and len(comment) == 0: - return + return False # Check message message_id = generate_nonce_hash(nonce) @@ -83,4 +83,4 @@ def add_chat_message(user, nonce, comment, ignore_empty=False): }, ) - return markup + return True diff --git a/anonstream/routes/nojs.py b/anonstream/routes/nojs.py index 707a5c9..c8f0be3 100644 --- a/anonstream/routes/nojs.py +++ b/anonstream/routes/nojs.py @@ -95,7 +95,7 @@ async def nojs_submit_message(user): ) except Rejected as e: notice, *_ = e.args - state_id = add_state(user, notice=notice) + state_id = add_state(user, notice=notice, comment=comment) else: deverify(user) state_id = None diff --git a/anonstream/static/anonstream.js b/anonstream/static/anonstream.js index 5299315..6e547cc 100644 --- a/anonstream/static/anonstream.js +++ b/anonstream/static/anonstream.js @@ -9,15 +9,18 @@ const jsmarkup_info = '
'; const jsmarkup_info_title = '
'; const jsmarkup_chat_messages = '
    '; const jsmarkup_chat_form = `\ -
    +
    - Not connected to chat + Not connected to chat
    + + + -
    `; +`; const insert_jsmarkup = () => { if (document.getElementById("style-color") === null) { @@ -253,6 +256,41 @@ const update_user_tripcodes = (token_hash=null) => { } } +const chat_form_captcha_digest = document.getElementById("chat-form_js__captcha-digest"); +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.alt = "Loading..."; +}); +chat_form_captcha_image.addEventListener("load", (event) => { + chat_form_captcha_image.removeAttribute("alt"); +}); +chat_form_captcha_image.addEventListener("error", (event) => { + chat_form_captcha_image.alt = "Captcha failed to load"; +}); +const enable_captcha = (digest) => { + chat_form_captcha_digest.value = digest; + chat_form_captcha_digest.disabled = false; + chat_form_captcha_answer.value = ""; + chat_form_captcha_answer.required = true; + chat_form_captcha_answer.disabled = false; + 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.dataset.captcha = ""; +} +const disable_captcha = () => { + chat_form.removeAttribute("data-captcha"); + chat_form_captcha_digest.disabled = true; + chat_form_captcha_answer.disabled = true; + chat_form_comment.required = true; + chat_form_captcha_digest.value = ""; + chat_form_captcha_answer.value = ""; + chat_form_captcha_answer.required = false; + chat_form_captcha_image.removeAttribute("alt"); + chat_form_captcha_image.removeAttribute("src"); +} + const on_websocket_message = (event) => { console.log("websocket message", event); const receipt = JSON.parse(event.data); @@ -264,9 +302,11 @@ const on_websocket_message = (event) => { case "init": console.log("ws init", receipt); - chat_form_nonce.value = receipt.nonce; info_title.innerText = receipt.title; + chat_form_nonce.value = receipt.nonce; + receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest); + default_name = receipt.default; max_chat_scrollback = receipt.scrollback; users = receipt.users; @@ -303,20 +343,15 @@ const on_websocket_message = (event) => { case "ack": console.log("ws ack", receipt); - if (chat_form_nonce.value === receipt.nonce) { + const existing_nonce = chat_form_nonce.value; + if (receipt.clear && receipt.nonce === existing_nonce) { chat_form_comment.value = ""; } else { - console.log("nonce does not match ack", chat_form_nonce, receipt); + console.log("nonce does not match ack", existing_nonce, receipt); } - chat_form_submit.disabled = false; chat_form_nonce.value = receipt.next; - break; - - case "reject": - console.log("ws reject", receipt); - alert(`Rejected: ${receipt.notice}`); + receipt.digest === null ? disable_captcha() : enable_captcha(receipt.digest); chat_form_submit.disabled = false; - chat_form_nonce.value = receipt.next; break; case "chat": @@ -362,13 +397,13 @@ const connect_websocket = () => { return; } chat_live_ball.style.borderColor = "gold"; - chat_live_status.innerText = "Connecting to chat..."; + chat_live_status.innerHTML = "Waiting... Connecting to chat..."; ws = new WebSocket(`ws://${document.domain}:${location.port}/live?token=${encodeURIComponent(token)}`); ws.addEventListener("open", (event) => { console.log("websocket open", event); chat_form_submit.disabled = false; chat_live_ball.style.borderColor = "green"; - chat_live_status.innerText = "Connected to chat"; + chat_live_status.innerHTML = "Connected to chat"; // When the server is offline, a newly opened websocket can take a second // to close. This timeout tries to ensure the backoff doesn't instantly // (erroneously) reset to 2 seconds in that case. @@ -384,7 +419,7 @@ const connect_websocket = () => { console.log("websocket close", event); chat_form_submit.disabled = true; chat_live_ball.style.borderColor = "maroon"; - chat_live_status.innerText = "Disconnected from chat"; + chat_live_status.innerHTML = "Failed to connect Disconnected from chat"; if (!ws.successor) { ws.successor = true; setTimeout(connect_websocket, websocket_backoff); @@ -395,7 +430,7 @@ const connect_websocket = () => { console.log("websocket error", event); chat_form_submit.disabled = true; chat_live_ball.style.borderColor = "maroon"; - chat_live_status.innerText = "Error connecting to chat"; + chat_live_status.innerHTML = "Error connecting to chat"; }); ws.addEventListener("message", on_websocket_message); } @@ -409,7 +444,7 @@ 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 = {comment: chat_form_comment.value, nonce: chat_form_nonce.value}; + const payload = Object.fromEntries(new FormData(chat_form)); chat_form_submit.disabled = true; ws.send(JSON.stringify(payload)); }); diff --git a/anonstream/static/style.css b/anonstream/static/style.css index d408b9f..bc716b6 100644 --- a/anonstream/static/style.css +++ b/anonstream/static/style.css @@ -87,38 +87,6 @@ noscript { padding: 0.5rem 0; border-bottom: var(--chat-border); } -#chat-form_js { - display: grid; - grid-template: auto var(--button-height) / auto 5rem; - grid-gap: 0.375rem; - margin: 0 0.5rem 0.5rem 0.5rem; -} -#chat-form_js__submit { - grid-column: 2 / span 1; -} -#chat-form_js__comment { - grid-column: 1 / span 2; - background-color: #434347; - border-radius: 4px; - border: 2px solid transparent; - transition: 0.25s; - max-height: max(37.5vh, 16ch); - min-height: 1.75ch; - height: 6ch; - padding: 0.675rem; - color: #c3c3c7; - resize: vertical; -} -#chat-form_js__comment:not(:focus):hover { - border-color: #737377; -} -#chat-form_js__comment:focus { - background-color: black; - border-color: #3584e4; -} -#chat-form_nojs { - height: 13ch; -} #chat__messages { position: relative; } @@ -163,7 +131,55 @@ noscript { font-size: 9pt; cursor: default; } +#chat-form_js { + display: grid; + grid-template-columns: 1fr min-content min-content 5rem; + grid-template-rows: auto var(--button-height); + grid-gap: 0.375rem; + margin: 0 0.5rem 0.5rem 0.5rem; +} +#chat-form_js__submit { + grid-column: 2 / span 1; +} +#chat-form_js__comment { + grid-column: 1 / span 4; + background-color: #434347; + border-radius: 4px; + border: 2px solid transparent; + transition: 0.25s; + max-height: max(37.5vh, 16ch); + min-height: 1.75ch; + height: 6ch; + padding: 0.675rem; + color: #c3c3c7; + resize: vertical; +} +#chat-form_js__comment:not(:focus):hover { + border-color: #737377; +} +#chat-form_js__comment:focus { + background-color: black; + border-color: #3584e4; +} +#chat-form_js__captcha-image { + color: inherit; + font-size: 8pt; +} +#chat-form_js__captcha-answer { + width: 8ch; +} +#chat-form_js__submit { + grid-column: 4; +} +#chat-form_js:not([data-captcha]) > #chat-form_js__captcha-image, +#chat-form_js:not([data-captcha]) > #chat-form_js__captcha-answer { + display: none; +} +#chat-form_nojs { + height: 13ch; +} #chat-live { + position: relative; font-size: 9pt; line-height: var(--button-height); } @@ -174,6 +190,18 @@ noscript { margin-right: 2px; animation: 3s infinite glow; } +#chat-live__status { + position: absolute; + left: 13px; + display: inline-grid; + grid-auto-flow: column; + height: 100%; + align-content: center; + line-height: 1.1875; +} +#chat-live__status [data-verbose="false"] { + display: none; +} @keyframes glow { 0% {filter: brightness(100%)} 50% {filter: brightness(150%)} @@ -250,6 +278,12 @@ footer { border-left: var(--chat-border); min-height: 100%; } + #chat-form_js[data-captcha] #chat-live__status [data-verbose="true"] { + display: none; + } + #chat-form_js[data-captcha] #chat-live__status [data-verbose="false"] { + display: inline; + } #nochat:target { --chat-width: 0px; } diff --git a/anonstream/utils/websocket.py b/anonstream/utils/websocket.py index 26295a0..5f4cdb5 100644 --- a/anonstream/utils/websocket.py +++ b/anonstream/utils/websocket.py @@ -13,7 +13,7 @@ def parse_websocket_data(receipt): if not isinstance(nonce, str): raise Malformed('malformed nonce') - digest = receipt.get('digest', '') - answer = receipt.get('answer', '') + digest = receipt.get('captcha-digest', '') + answer = receipt.get('captcha-answer', '') return nonce, comment, digest, answer diff --git a/anonstream/websocket.py b/anonstream/websocket.py index 686b4dc..94e0bc4 100644 --- a/anonstream/websocket.py +++ b/anonstream/websocket.py @@ -45,29 +45,28 @@ async def websocket_inbound(queue, user): } else: try: - verify(user, digest, answer) + verification_happened = verify(user, digest, answer) except BadCaptcha as e: notice, *_ = e.args - payload = { - 'type': 'captcha', - 'notice': notice, - 'digest': get_random_captcha_digest_for(user), - } else: try: - markup = add_chat_message(user, nonce, comment) + message_was_added = add_chat_message( + user, + nonce, + comment, + ignore_empty=verification_happened, + ) except Rejected as e: notice, *_ = e.args - payload = { - 'type': 'reject', - 'notice': notice, - } else: deverify(user) - payload = { - 'type': 'ack', - 'nonce': nonce, - 'next': generate_nonce(), - 'digest': get_random_captcha_digest_for(user), - } + notice = None + payload = { + 'type': 'ack', + 'nonce': nonce, + 'next': generate_nonce(), + 'notice': notice, + 'clear': message_was_added, + 'digest': get_random_captcha_digest_for(user), + } queue.put_nowait(payload)