Codecha-プログラマキャプチャ、またはAPIを設計しない方法

私のサイトでは、プログラマーのキャプチャーであるCodechaを使用しています。 これは、選択したプログラミング言語の1つで問題を解決する関数の本体を記述する必要があるソリューションのためのユニークなキャプチャです。



KDPV-この同じキャプチャのウィジェット



たとえば、Google reCAPTCHA(タスクのセットは限られているため、非常に迅速に解決し、すぐに答えを出すことができます)ほどボットに対する信頼性の高い保護を提供しませんが、プログラマー以外の人(特定のカテゴリのフォーラム、学生の大勢に対してラボでの作業を行うことは、スパムボットよりも深刻な問題です。 しかし、メモはそれについてではありません。



Codechaの使用中に発生した問題は、captchaウィジェットとサーバーの両方の完全に不便なAPIです。



開始するには、ウィジェットの接続方法を簡単に確認することをお勧めします。 ドキュメントをコピーするのではなく、最も重要なことだけをコピーします。



したがって、登録して、フォームのどこかに次のコードを追加します。



<script type="text/javascript" src="//codecha.org/api/challenge?k=YOUR_PUBLIC_KEY"> </script>
      
      





すべてがウィジェットにあります。 ここで、フォームを送信するとき、 codecha_challenge_fieldおよびcodecha_response_fieldフィールドを取得し、それらを検証のためにサーバーに送信する必要があります。 以降、サーバーコードはNode.jsにあります。



フォームを解析し、検証関数を呼び出します(promiseとq-ioを使用 ):



 var HTTP = require("q-io/http"); var checkCaptcha = function(req, fields) { var challenge = fields.codecha_challenge_field; var response = fields.codecha_response_field; if (!challenge) return Promise.reject("Captcha challenge is empty"); if (!response) return Promise.reject("Captcha is empty", "error"); var body = `challenge=${challenge}&response=${response}&remoteip=${req.ip}&privatekey=PRIVTE_KEY`; var url = "http://codecha.org/api/verify"; return HTTP.request({ url: url, method: "POST", body: [body], headers: { "Content-Type": "application/x-www-form-urlencoded", "Content-Length": Buffer.byteLength(body) }, timeout: (15 * 1000) //15  }).then(function(response) { if (response.status != 200) return Promise.reject("Failed to check captcha"); return response.body.read("utf8"); }).then(function(data) { var result = data.toString(); if (result.replace("true") == result) return Promise.reject("Invalid captcha"); return Promise.resolve(); }); };
      
      





成功した場合、文字列は「true」 、それ以外の場合は文字列「false」を返します



すべてが単純なように思えますが、どのような質問がありますか? しかし、違います。 ユーザーがAJAXを使用して1つのメッセージを送信し、ページを更新せずに次のメッセージを送信するときに、楽しみが始まります。 たとえば、reCAPTCHAはgrecaptcha.resetメソッドを提供しますが、 Codechaにはそのようなメソッドはありません。 キャプチャを解決すると、そのHTMLはすべて削除され、タスクの成功したソリューションを報告するテキストのみが残ります。 ページを更新する必要があります。



たとえば、サーバーでキャプチャをチェックしているときにエラーが発生した場合も同じことが起こります。 この場合、Codechaサーバー上のタスクとソリューションのペアは無効になり、同じペアを再チェックすると「false」が生成されます 。 ユーザーは再びページを更新する必要があります(この場合、フォームに入力されたすべてのデータも失われます)。



良くない たぶんある種の隠されたウィジェットAPIがありますか そうでなければなりません。 しかし、違います。



フォームに挿入されたスクリプトのコードを確認します
 var codecha = { language: "PHP", publicKey: "e37ea65c651a4eada4d5d4e97ae92d90", fieldPrefix: "codecha_", base_url: "//codecha.org", spinner_path: "/static/ajax-loader.gif", css_path: "/static/widget.css", survey: true, callbacks: {}, }; codecha.callbacks.hideErrorOverlay = function() { codecha.errorOverlayDiv.hidden = true; return false; } codecha.callbacks.codeSubmit = function() { var xhr = codecha.CORCSRequest(codecha.base_url + "/api/code"); codecha.disable(); var params = { 'challenge': codecha.challenge, 'code': codecha.codeArea.value }; xhr.send(codecha.serialize(params)); return false; }; codecha.callbacks.choseLanguage = function() { codecha.languageSelector.hidden = false; codecha.languageSelector.style.display = ''; codecha.changeChallenge.value = "\u2713"; codecha.button.disabled = true; codecha.changeChallenge.onclick = codecha.callbacks.requestNewChallenge; return false; }; codecha.callbacks.requestNewChallenge = function() { codecha.disable(); codecha.setStatus("waiting"); codecha.languageSelector.hidden = true; codecha.languageSelector.style.display = 'none'; codecha.changeChallenge.value = "change lang"; var lang = codecha.languageSelector[codecha.languageSelector.selectedIndex].value; var xhr = codecha.CORCSRequest(codecha.base_url + "/api/change"); var params = { 'challenge': codecha.challenge, 'k': codecha.publicKey, 'lang': lang }; xhr.send(codecha.serialize(params)); codecha.changeChallenge.onclick = codecha.callbacks.choseLanguage; return false; }; codecha.callbacks.textAreaKeyPress = function(ev) { object = codecha.codeArea; if (ev.keyCode == 9) { start = object.selectionStart; end = object.selectionEnd; object.value = object.value.substring(0, start) + "\t" + object.value.substr(end); object.setSelectionRange(start + 1, start + 1); object.selectionStart = object.selectionEnd = start + 1; return false; } return true; }; codecha.callbacks.updateState = function() { var xhr = codecha.CORCSRequest(codecha.base_url + "/api/state"); xhr.send(codecha.serialize({ 'challenge': codecha.challenge })); return false; }; codecha.callbacks.sendSurvey = function() { var xhr = codecha.CORCSRequest(codecha.base_url + "/api/survey"); var mark = codecha.surveyMark[codecha.surveyMark.selectedIndex].value; var params = { 'challenge': codecha.challenge, 'response': codecha.response, 'mark': mark, 'opinion' : codecha.surveyOpinion.value }; xhr.send(codecha.serialize(params)); return false; }; codecha.callbacks.switchToRecaptcha = function() { var challengeField = document.getElementById(codecha.fieldPrefix + "challenge_field"); var responseField = document.getElementById(codecha.fieldPrefix + "response_field"); codecha.removeElement(codecha.mainDiv); codecha.removeElement(codecha.recaptchaSwitch); codecha.removeElement(challengeField); codecha.removeElement(responseField); codecha.recaptchaDiv.hidden = false; var xhr = codecha.CORCSRequest(codecha.base_url + "/api/recaptchify"); var params = { 'challenge': codecha.challenge }; xhr.send(codecha.serialize(params)); return false; }; codecha.removeElement = function(element) { element.parentElement.removeChild(element); }; codecha.removeRecatpcha = function() { this.removeElement(this.recaptchaDiv); }; codecha.escape = function(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; }; codecha.serialize = function(obj) { array = []; for (key in obj) { array[array.length] = encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]); } var result = array.join("&"); result = result.replace(/%20/g, "+"); return result; }; codecha.enable = function() { this.button.disabled = false; this.changeChallenge.disabled = false; this.codeArea.disabled = false; this.spinner.hidden = true; }; codecha.disable = function() { this.button.disabled = true; this.changeChallenge.disabled = true; this.codeArea.disabled = true; this.spinner.hidden = false; }; codecha.inject_css = function() { var css_link=document.createElement("link"); css_link.setAttribute("rel", "stylesheet"); css_link.setAttribute("type", "text/css"); css_link.setAttribute("href", codecha.base_url + codecha.css_path); document.getElementsByTagName("head")[0].appendChild(css_link); }; codecha.setStatus = function(state) { this.statusSpan.innerHTML = state; }; codecha.setResponseFields = function() { var challengeField = document.getElementById(this.fieldPrefix + "challenge_field"); var responseField = document.getElementById(this.fieldPrefix + "response_field"); challengeField.value = this.challenge; responseField.value = this.response; }; codecha.setChallenge = function(uuid, language_name, wording, top, sampleCode, bottom) { this.challenge = uuid; this.wordingDiv.innerHTML = "<strong>" + language_name + ":</strong> " + wording; this.codeAreaTop.innerHTML = "<pre>\n"+this.escape(top)+"</pre>"; this.codeArea.value = sampleCode; this.codeAreaBottom.innerHTML = this.escape(bottom); if (top.length > 0) { this.codeAreaTop.hidden = false; } else { this.codeAreaTop.hidden = true; } if (bottom.length > 0) { this.codeAreaBottom.hidden = false; } else { this.codeAreaBottom.hidden = true; } }; codecha.showErrorMessage = function(message) { this.errorMessageDiv.innerHTML = message; this.errorOverlayDiv.hidden = false; }; codecha.showSurvey = function() { codecha.mainDiv.innerHTML = "\ <strong>Challenge completed! You may proceed.</strong> \ If you have some spare time you may help us improve our widget by answearing any question below. \ Challenge was: \ <select id=\"codecha_survey_mark_selector\"> \ <option value=\"5\">a way too hard</option> \ <option value=\"4\">a bit too hard</option> \ <option value=\"3\" selected>perfect</option> \ <option value=\"2\">a bit too easy</option> \ <option value=\"1\">a way too easy</option> \ </select> \ How do you like our widget? \ <textarea id=\"codcha_survey_opinion_area\" name=\"codcha_survey_opinion_area\">I like/dislike it because...</textarea> \ <input type=\"submit\" class=\"codecha_button\" name=\"codecha_survey_submit\" id=\"codecha_survey_submit\" value=\"SUBMIT\"/>\ "; codecha.surveySubmit = document.getElementById("codecha_survey_submit"); codecha.surveyMark = document.getElementById("codecha_survey_mark_selector"); codecha.surveyOpinion = document.getElementById("codcha_survey_opinion_area"); codecha.surveySubmit.onclick = codecha.callbacks.sendSurvey; }; codecha.CORCSRequest = function (url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { xhr.open("POST", url, true); } else if (typeof XDomainRequest != "undefined") { xhr = new XDomainRequest(); xhr.open("POST", url); } else { xhr = null; } xhr.onload = function() { eval(this.responseText); }; xhr.onerror = function() { alert("Error!"); codecha.enable(); }; xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); return xhr; }; codecha.init = function() { document.write( "<input type=\"hidden\" id=\"" + this.fieldPrefix + "challenge_field\" /name=\"" + this.fieldPrefix + "challenge_field\" />", "<input type=\"hidden\" id=\"" + this.fieldPrefix + "response_field\" name=\"" + this.fieldPrefix + "response_field\" />", "<div id=\"codecha_widget\">", "<div id=\"codecha_error_overlay\">", "<a href=\"#\" id=\"codecha_error_overlay_hide\">hide</a>", "<div id=\"codecha_error_message\"></div>", "</div>", "<div id=\"codecha_wording\"></div>", "<div id=\"codecha_code_area_top\"></div>", "<textarea name=\"codecha_code_area\" id=\"codecha_code_area\">", "</textarea>", "<div id=\"codecha_code_area_bottom\"></div>", "<div id=\"codecha_bottom_container\">", "<a title=\"click to learn more\" href=\"" + codecha.base_url + "/about\" target=\"_blank\" id=\"codecha_about\">Codecha</a>", "<div id=\"codecha_bottom\">", "<span id=\"codecha_spinner\">", "<span id=\"codecha_status\">waiting</span>", "<img alt=\"spinner\" src=\"" + codecha.base_url + codecha.spinner_path + "\" />", "</span>", "<select id=\"codecha_language_selector\">", "<option value=\"c\" >C/C++</option>", "<option value=\"java\" >Java</option>", "<option value=\"python\" >Python</option>", "<option value=\"ruby\" >Ruby</option>", "<option value=\"php\" selected >PHP</option>", "<option value=\"haskell\" >Haskell</option>", "</select>", "<input type=\"submit\" class=\"codecha_button\" name=\"codecha_change_challenge\" id=\"codecha_change_challenge\" title=\"request new challenge\" value=\"change lang\"/>", "<input type=\"submit\" class=\"codecha_button\" name=\"codecha_code_submit_button\" id=\"codecha_code_submit_button\" value=\"VERIFY\"/>", "</div>", "</div>", "</div>", "<div id=\"codecha_recaptcha\">", "</div>" ); this.mainDiv = document.getElementById("codecha_widget"); this.codeArea = document.getElementById("codecha_code_area"); this.codeAreaTop = document.getElementById("codecha_code_area_top"); this.codeAreaBottom = document.getElementById("codecha_code_area_bottom"); this.wordingDiv = document.getElementById("codecha_wording"); this.errorOverlayDiv = document.getElementById("codecha_error_overlay"); this.errorMessageDiv = document.getElementById("codecha_error_message"); this.errorHide = document.getElementById("codecha_error_overlay_hide"); this.button = document.getElementById("codecha_code_submit_button"); this.spinner = document.getElementById("codecha_spinner"); this.statusSpan = document.getElementById("codecha_status"); this.languageSelector = document.getElementById("codecha_language_selector"); this.recaptchaDiv = document.getElementById("codecha_recaptcha"); this.changeChallenge = document.getElementById("codecha_change_challenge"); this.changeChallenge.onclick = codecha.callbacks.choseLanguage; this.button.onclick = codecha.callbacks.codeSubmit; this.errorHide.onclick = codecha.callbacks.hideErrorOverlay; this.codeArea.onkeydown = codecha.callbacks.textAreaKeyPress; this.errorOverlayDiv.hidden = true; this.spinner.hidden = true; this.languageSelector.hidden = true; this.languageSelector.style.display = 'none'; this.inject_css(); this.enable(); codecha.setChallenge("d847842d3225459582722c8695ef8523", "PHP", "For given numbers \u0022a\u0022 and \u0022b\u0022 write a function named \u0022lessab\u0022 that returns the value \u00221\u0022 if \u0022a\u0022 is less than \u0022b\u0022, and returns the value \u00220\u0022 otherwise.\u000A", "function lessab($a,$b) {\u000A", "# put your code here\u000A", "\u000A}\u000A"); }; codecha.init();
      
      







スポイラーを開き、Ctrl + Fを押して「codecha.init」と入力します。 はい、はい、それは本当です。 自分で試すことができます。 これはまさに、文字列形式のHTMLコードであり、 document.writeを使用してページに追加されます 。 それで何が悪いのかと聞かないでください。



しかし、それだけではありません! (c)奇跡デバイスの広告



たとえば、言語の変更はどのように行われますか? リクエストを送信してから...ドラムロール...



 xhr.onload = function() { eval(this.responseText); };
      
      





そうそう! ここで終了することは可能ですが、サボテンを最後まで食べることにしました。 このために、Codechaのラッパーを作成しました。 私はあまり大げさなことはしないで、すぐに説明付きのコードを提供します。



したがって、HTMLウィジェットは次のとおりです。



 <div id="captcha" class="codechaContainer"> <input type="hidden" id="codecha_public_key" value="PUBLIC_KEY" /> <input type="hidden" id="codecha_challenge_field" name="codecha_challenge_field" value="CHALLENGE" /> <input type="hidden" id="codecha_response_field" name="codecha_response_field" /> <div id="codecha_ready_widget" style="display: none;"><strong>Challenge completed! You may proceed.</strong></div> <div id="codecha_widget"> <div id="codecha_error_overlay" hidden="true"> <a id="codecha_error_overlay_hide" href="javascript:void(0);" onclick="codecha.hideErrorOverlay();">hide</a> <div id="codecha_error_message"></div> </div> <div id="codecha_wording"></div> <div id="codecha_code_area_top"></div> <textarea id="codecha_code_area" name="codecha_code_area"></textarea> <div id="codecha_code_area_bottom"></div> <div id="codecha_bottom_container"> <a title="click to learn more" href="//codecha.org/about" target="_blank" id="codecha_about">Codecha</a> <div id="codecha_bottom"> <span id="codecha_spinner" hidden="true"> <span id="codecha_status">waiting</span><img src="//codecha.org/static/ajax-loader.gif" /> </span> <select id="codecha_language_selector" hidden="true" style="display: none;"> <option value="c" selected="true">C/C++</option> <option value="java">Java</option> <option value="python">Python</option> <option value="ruby">Ruby</option> <option value="php">PHP</option> <option value="haskell">Haskell</option> </select> <input type="submit" class="codecha_button" name="codecha_change_challenge" id="codecha_change_challenge" title="request new challenge" value="change lang" onclick="codecha.chooseLanguage(); return false;" /> <input type="submit" class="codecha_button" name="codecha_code_submit_button" id="codecha_code_submit_button" value="VERIFY" onclick="codecha.codeSubmit(); return false;" /> </div> </div> </div> </div>
      
      





クライアントスクリプト:



 var codecha = {}; //     function id(_id) { return document.getElementById(_id); } function node(type, text) { if (typeof type != "string") return null; type = type.toUpperCase(); return ("TEXT" == type) ? document.createTextNode(text ? text : "") : document.createElement(type); }; function post(action, data) { return $.ajax(action, { type: "POST", data: data, dataType: "text" }); }; codecha.mustRequestNewChallenge = false; codecha.serialize = function(obj) { var array = []; for (key in obj) array[array.length] = encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]); var result = array.join("&"); result = result.replace(/%20/g, "+"); return result; }; codecha.escape = function(str) { var div = node("div"); div.appendChild(node("text", str)); return div.innerHTML; }; codecha.enable = function() { id("codecha_code_submit_button").disabled = false; id("codecha_change_challenge").disabled = false; id("codecha_code_area").disabled = false; id("codecha_spinner").hidden = true; }; codecha.disable = function() { id("codecha_code_submit_button").disabled = true; id("codecha_change_challenge").disabled = true; id("codecha_code_area").disabled = true; id("codecha_spinner").hidden = false; }; codecha.hideErrorOverlay = function() { id("codecha_error_overlay").hidden = true; } codecha.setStatus = function(state) { id("codecha_status").innerHTML = state; }; codecha.updateState = function() { post("//codecha.org/api/state", codecha.serialize({ 'challenge': id("codecha_challenge_field").value })).then(function(response) { var match = /codecha\.response\s*\=\s"([^"]+)"/gi.exec(response); if (match) { codecha.mustRequestNewChallenge = true; id("codecha_response_field").value = match[1]; id("codecha_widget").style.display = "none"; id("codecha_ready_widget").style.display = ""; } else { eval(response.replace(".callbacks", "")); } }).catch(function(err) { console.log(err); }); }; codecha.showErrorMessage = function(message) { id("codecha_error_message").innerHTML = message; id("codecha_error_overlay").hidden = false; }; codecha.setChallenge = function(uuid, langName, wording, top, sampleCode, bottom) { id("codecha_challenge_field").value = uuid; id("codecha_wording").innerHTML = "<strong>" + langName + ":</strong> " + wording; id("codecha_code_area_top").innerHTML = "<pre>\n"+this.escape(top)+"</pre>"; id("codecha_code_area").value = sampleCode; id("codecha_code_area_bottom").innerHTML = codecha.escape(bottom); id("codecha_code_area_top").hidden = (top.length <= 0); id("codecha_code_area_bottom").hidden = (bottom.length <= 0); }; codecha.choseLanguage = function() { id("codecha_language_selector").hidden = false; id("codecha_language_selector").style.display = ""; id("codecha_change_challenge").value = "\u2713"; id("codecha_code_submit_button").disabled = true; id("codecha_change_challenge").onclick = codecha.requestNewChallenge; return false; }; codecha.requestNewChallenge = function() { codecha.disable(); codecha.setStatus("waiting"); var select = id("codecha_language_selector"); select.hidden = true; select.style.display = "none"; id("codecha_change_challenge").value = "change lang"; id("codecha_change_challenge").onclick = codecha.choseLanguage; id("codecha_response_field").value = ""; var p; if (!codecha.mustRequestNewChallenge) { p = Promise.resolve(); } else { p = Promise.resolve().then(function() { return $.getJSON("/api/codechaChallenge.json"); }).then(function(model) { codecha.mustRequestNewChallenge = false; id("codecha_challenge_field").value = model; return Promise.resolve(); }); } p.then(function() { var params = { "challenge": id("codecha_challenge_field").value, "k": id("codecha_public_key").value, "lang": select.options[select.selectedIndex].value }; return post("//codecha.org/api/change", codecha.serialize(params)); }).then(function(response) { eval(response); }).catch(function(err) { console.log(err); }); return false; }; codecha.codeSubmit = function() { codecha.disable(); var params = { "challenge": id("codecha_challenge_field").value, "code": id("codecha_code_area").value }; post("//codecha.org/api/code", codecha.serialize(params)).then(function(response) { codecha.setStatus("sending"); setTimeout(codecha.updateState, 1000); }).catch(function(err) { console.log(err); }); }; (function() { var link = node("link"); link.setAttribute("rel", "stylesheet"); link.setAttribute("type", "text/css"); link.setAttribute("href", "//codecha.org/static/widget.css"); document.querySelector("head").appendChild(link); })(); window.addEventListener("load", function load() { window.removeEventListener("load", load, false); codecha.disable(); codecha.requestNewChallenge(); }, false); var reloadCaptcha = function() { codecha.requestNewChallenge(); id("codecha_widget").style.display = ""; id("codecha_ready_widget").style.display = "none"; };
      
      





サーバーコード:



 var requestChallenge = function(req) { var url = `http://codecha.org/api/challenge?k=PUBLIC_KEY`; return HTTP.request({ url: url, timeout: (15 * 1000) }).then(function(response) { if (response.status != 200) return Promise.reject("Failed to get challenge"); return response.body.read("utf8"); }).then(function(data) { var match = /codecha.setChallenge\("([^"]+)"/gi.exec(data.toString()); if (!match) return Promise.reject("Captcha server error"); return Promise.resolve(match[1]); }); };
      
      





ここで何が起こっているかを理解します。 まず、フォームに挿入されたスクリプトのアドレスに移動し、正規表現を使用してジョブ識別子を分離します。 次に、HTMLテンプレートの識別子を置き換え、最後にページをユーザーに渡します。



クライアントスクリプトは、ジョブを変更する要求を送信します。 新しいジョブをリクエストするときは、前のジョブの識別子を渡す必要があることに注意することが重要です。 新しいタスクを取得し、ウィジェットに置き換えます。 しかし、最も興味深いのは次です。



ユーザーがタスクを入力し、Codechaサーバーがそれをチェックした後(すべてが元のウィジェットとほとんど同じです)、インターフェイス要素を削除せず、単に非表示にします。 AJAXリクエストでサーバーにメッセージを送信した後、 reloadCaptcha関数を呼び出して、サーバーからジョブ識別子を要求し、その識別子を使用して、Codechaサーバーから新しいジョブを受信し、ウィジェットに再度入力します。 同時に、ページを更新する必要がないため、ユーザーはドライで快適に感じます。



最後に、いくつかの推奨事項を示します。



  1. サーバーからの応答を解釈するために決してevalを使用しないでください。 これは開発者に頭痛の種を追加するだけですが、APIを干渉から保護する助けにはなりません。
  2. 製品がどのように使用されるかを考えてください。 すべてのオプションを検討してください。もっとも信じがたいこともあります(ただし、メモに記載されているケースはごく普通のことです)。 すべての状況でAPIを使用できるようにしてください。
  3. 以前のデータに関する情報について、以前のデータとはまったく関係なく、新しいデータの受信に結び付けないでください。 繰り返しますが、これは頭痛の種になりますが、何に対しても保護しません。
  4. APIを干渉から保護することにした場合は、少なくとも難読化を使用してください。 これがなければ、これは自分の時間と他の人の時間の無意味な無駄です。



All Articles