commit 563399081b51443092d82fa29043316df57f26d9 Author: Skylar Ittner Date: Fri Jun 9 03:33:56 2017 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f65e2ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +nbproject/private +settings.php +database.mwb.bak +debug +vendor \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..df0407f --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (C) 2017 Netsyms Technologies. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL NETSYMS TECHNOLOGIES BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name and other identifying marks of Netsyms Technologies shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Netsyms Technologies. \ No newline at end of file diff --git a/api.php b/api.php new file mode 100644 index 0000000..ced2072 --- /dev/null +++ b/api.php @@ -0,0 +1,90 @@ + "OK", "pong" => true]; + exit(json_encode($out)); + case "new": + // generate unique session ID that has an essentially zero chance of being a duplicate. + // Contains a hash of a secure random number, a hash of the user's IP, and 23 uniqid() characters. + $skey = uniqid(substr(hash("md5", mt_rand()), 3, 5) . hash("md5", getUserIP()), true); + $answers = $database->select('answers', ['aid', 'aname']); + shuffle($answers); + $answers = array_slice($answers, 0, 5); + //var_dump($answers); + $correct_answer = $answers[mt_rand(0, count($answers) - 1)]; + $scrambled = ["real" => [], "fake" => []]; + foreach ($answers as $a) { + $scrambled["real"][] = $a['aid']; + $scrambled["fake"][] = substr(hash("md5", mt_rand()), 0, 20); + } + $database->insert("sessions", ["skey" => $skey, "aid" => $correct_answer['aid'], "expired" => 0, "#timestamp" => "NOW()"]); + $sid = $database->id(); + $scrambled_insert = []; + for ($i = 0; $i < count($scrambled['real']); $i++) { + $scrambled_insert[] = ["sid" => $sid, "aid" => $scrambled['real'][$i], "acode" => $scrambled['fake'][$i]]; + } + $database->insert("scrambled_answers", $scrambled_insert); + $resp = [ + "session" => $skey, + "question" => $correct_answer['aname'], + "answers" => $scrambled["fake"] + ]; + exit(json_encode($resp)); + case "img": + if (!$database->has('sessions', ['skey' => $VARS['s']])) { + sendError("Missing or invalid session ID.", "client"); + } + $sid = $database->get('sessions', 'sid', ['skey' => $VARS['s']]); + if (!$database->has("scrambled_answers", ["AND" => ["sid" => $sid, "acode" => $VARS['c']]])) { + sendError("Missing or invalid image code.", "client"); + } + $imgid = $database->get("scrambled_answers", ["[>]answers" => ["aid" => "aid"]], 'aimg', ["AND" => ["sid" => $sid, "acode" => $VARS['c']]]); + /* Load image, add some black/white noise, and send */ + header('Content-Type: image/png'); + $imgpath = __DIR__ . "/images/" . $imgid . ".png"; + if (DEBUG) { + file_put_contents("debug", $imgpath . "\n", FILE_APPEND); + } + $img = imagecreatefrompng($imgpath); + imageAlphaBlending($img, true); + imageSaveAlpha($img, true); + $black = imagecolorallocate($img, 0, 0, 0); + $white = imagecolorallocate($img, 255, 255, 255); + for ($i = 0; $i < 512; $i++) { + imagesetpixel($img, mt_rand(0, 63), mt_rand(0, 63), $black); + } + for ($i = 0; $i < 256; $i++) { + imagesetpixel($img, mt_rand(0, 63), mt_rand(0, 63), $white); + } + imagepng($img); + exit(); + case "verify": + if (!$database->has('sessions', ['skey' => $VARS['session_id']])) { + echo json_encode(["session" => $VARS['session_id'], "result" => false, "msg" => "Session invalid."]); + exit(); + } + $sid = $database->get('sessions', 'sid', ['skey' => $VARS['session_id']]); + $expired = ($database->get('sessions', 'expired', ['skey' => $VARS['session_id']]) == 1 ? true : false); + if ($expired) { + echo json_encode(["session" => $VARS['session_id'], "result" => false, "msg" => "Session key already used."]); + exit(); + } + if (!$database->has("scrambled_answers", ["AND" => ["sid" => $sid, "acode" => $VARS['answer_id']]])) { + echo json_encode(["session" => $VARS['session_id'], "result" => false, "msg" => "Answer invalid."]); + exit(); + } + $aid = $database->get('scrambled_answers', 'aid', ["AND" => ["sid" => $sid, "acode" => $VARS['answer_id']]]); + if ($database->has('sessions', ["AND" => ["sid" => $sid, "aid" => $aid]])) { + echo json_encode(["session" => $VARS['session_id'], "result" => true]); + } else { + echo json_encode(["session" => $VARS['session_id'], "result" => false, "msg" => "Answer incorrect."]); + } + $database->update("sessions", ['expired' => 1], ["sid" => $sid]); + exit(); + default: + sendError("Bad Request", "client"); +} \ No newline at end of file diff --git a/captcheck.js b/captcheck.js new file mode 100644 index 0000000..5a04c6c --- /dev/null +++ b/captcheck.js @@ -0,0 +1,58 @@ +window.onload = function () { + var api_url = "http://192.168.25.1/captcheck/api.php"; + var getJSON = function (url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if (this.readyState == 4) { + callback(this.status, this.responseText); + } + }; + xhr.send(); + }; + getJSON(api_url + "?action=new", function (status, json) { + /* Add custom styles */ + var styles = document.createElement('style'); + styles.innerHTML = ".captcheck_box {font-family: Ubuntu, Arial, sans-serif; border: 1px solid #e0e0e0; border-radius: 3px; display: inline-block; padding: 3px; margin: 5px 2px 5px 1px; background-color: #f5f5f5;} .captcheck_answer_label > input {visibility: hidden; position: absolute;} .captcheck_answer_label > input + img {cursor: pointer; border: 2px solid transparent; border-radius: 3px; min-width: 32px; width: 18%; max-width: 64px;} .captcheck_answer_label > input:checked + img {cursor: pointer; border: 2px solid #424242; border-radius: 3px;} .captcheck_error_message { color: red; }"; + document.body.appendChild(styles); + + /* Get captcha container div */ + var container = document.getElementById("captcheck_container"); + /* Create captcha div */ + var captcha = document.createElement("div"); + captcha.setAttribute("class", "captcheck_box"); + container.appendChild(captcha); + + if (status == 200) { + var data = JSON.parse(json); + /* Create answer buttons */ + var answers = ""; + for (var i = 0, len = data.answers.length; i < len; i++) { + var src = api_url + "?action=img&s=" + data.session + "&c=" + data.answers[i]; + answers += ""; + } + var answer_div = document.createElement("div"); + answer_div.innerHTML = answers; + /* Create question */ + var question_div = document.createElement("div"); + question_div.innerHTML = "Click on the " + data.question + ":"; + + /* Add question and answers */ + captcha.appendChild(question_div); + captcha.appendChild(answer_div); + + /* Add hidden session ID element */ + var skey_input = document.createElement("span"); + skey_input.innerHTML = ""; + captcha.appendChild(skey_input); + } else { + /* Add error message */ + captcha.innerHTML = "There was a problem loading the CAPTCHA."; + } + }); +} + +function chooseAnswer(ans) { + var box = document.getElementById("captcheck_answer_" + ans); + box.checked = true; +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7e61998 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "catfan/medoo": "^1.4" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..86b83e6 --- /dev/null +++ b/composer.lock @@ -0,0 +1,77 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "3d60b6d6d1ba750afa45d307e067f006", + "packages": [ + { + "name": "catfan/medoo", + "version": "v1.4.4", + "source": { + "type": "git", + "url": "https://github.com/catfan/Medoo.git", + "reference": "bcabbef4d8355d52fc4d19f17463e5e816c9ef44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/catfan/Medoo/zipball/bcabbef4d8355d52fc4d19f17463e5e816c9ef44", + "reference": "bcabbef4d8355d52fc4d19f17463e5e816c9ef44", + "shasum": "" + }, + "require": { + "ext-pdo": "*", + "php": ">=5.4" + }, + "suggest": { + "ext-pdo_dblib": "For MSSQL or Sybase database on Linux/UNIX platform", + "ext-pdo_mysql": "For MySQL or MariaDB database", + "ext-pdo_oci": "For Oracle database", + "ext-pdo_oci8": "For Oracle version 8 database", + "ext-pdo_pqsql": "For PostgreSQL database", + "ext-pdo_sqlite": "For SQLite database", + "ext-pdo_sqlsrv": "For MSSQL database on Windows platform" + }, + "type": "framework", + "autoload": { + "psr-4": { + "Medoo\\": "/src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Angel Lai", + "email": "angel@catfan.me" + } + ], + "description": "The lightest PHP database framework to accelerate development", + "homepage": "https://medoo.in", + "keywords": [ + "database", + "lightweight", + "mariadb", + "mssql", + "mysql", + "oracle", + "php framework", + "postgresql", + "sql", + "sqlite" + ], + "time": "2017-06-02T15:25:04+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/database.mwb b/database.mwb new file mode 100644 index 0000000..43ba555 Binary files /dev/null and b/database.mwb differ diff --git a/images/bolt.png b/images/bolt.png new file mode 100644 index 0000000..7879516 Binary files /dev/null and b/images/bolt.png differ diff --git a/images/building.png b/images/building.png new file mode 100644 index 0000000..7ac65df Binary files /dev/null and b/images/building.png differ diff --git a/images/camera.png b/images/camera.png new file mode 100644 index 0000000..bdcda02 Binary files /dev/null and b/images/camera.png differ diff --git a/images/circle.png b/images/circle.png new file mode 100644 index 0000000..9cb726b Binary files /dev/null and b/images/circle.png differ diff --git a/images/cloud.png b/images/cloud.png new file mode 100644 index 0000000..9237753 Binary files /dev/null and b/images/cloud.png differ diff --git a/images/cog.png b/images/cog.png new file mode 100644 index 0000000..e0386a9 Binary files /dev/null and b/images/cog.png differ diff --git a/images/cube.png b/images/cube.png new file mode 100644 index 0000000..e21607f Binary files /dev/null and b/images/cube.png differ diff --git a/images/envelope.png b/images/envelope.png new file mode 100644 index 0000000..aa570bf Binary files /dev/null and b/images/envelope.png differ diff --git a/images/female.png b/images/female.png new file mode 100644 index 0000000..a59e303 Binary files /dev/null and b/images/female.png differ diff --git a/images/file-o.png b/images/file-o.png new file mode 100644 index 0000000..cc03cc8 Binary files /dev/null and b/images/file-o.png differ diff --git a/images/flag.png b/images/flag.png new file mode 100644 index 0000000..635d51f Binary files /dev/null and b/images/flag.png differ diff --git a/images/globe.png b/images/globe.png new file mode 100644 index 0000000..9f55fc3 Binary files /dev/null and b/images/globe.png differ diff --git a/images/heart.png b/images/heart.png new file mode 100644 index 0000000..357e9d6 Binary files /dev/null and b/images/heart.png differ diff --git a/images/male.png b/images/male.png new file mode 100644 index 0000000..ece914b Binary files /dev/null and b/images/male.png differ diff --git a/images/mobile.png b/images/mobile.png new file mode 100644 index 0000000..c0c9818 Binary files /dev/null and b/images/mobile.png differ diff --git a/images/moon-o.png b/images/moon-o.png new file mode 100644 index 0000000..7ba1e22 Binary files /dev/null and b/images/moon-o.png differ diff --git a/images/paint-brush.png b/images/paint-brush.png new file mode 100644 index 0000000..263219f Binary files /dev/null and b/images/paint-brush.png differ diff --git a/images/pencil.png b/images/pencil.png new file mode 100644 index 0000000..490b958 Binary files /dev/null and b/images/pencil.png differ diff --git a/images/picture-o.png b/images/picture-o.png new file mode 100644 index 0000000..f6922d9 Binary files /dev/null and b/images/picture-o.png differ diff --git a/images/plane.png b/images/plane.png new file mode 100644 index 0000000..39b8763 Binary files /dev/null and b/images/plane.png differ diff --git a/images/print.png b/images/print.png new file mode 100644 index 0000000..34c4c46 Binary files /dev/null and b/images/print.png differ diff --git a/images/puzzle-piece.png b/images/puzzle-piece.png new file mode 100644 index 0000000..7426bdb Binary files /dev/null and b/images/puzzle-piece.png differ diff --git a/images/shopping-basket.png b/images/shopping-basket.png new file mode 100644 index 0000000..ad318e8 Binary files /dev/null and b/images/shopping-basket.png differ diff --git a/images/snowflake-o.png b/images/snowflake-o.png new file mode 100644 index 0000000..83d4440 Binary files /dev/null and b/images/snowflake-o.png differ diff --git a/images/square.png b/images/square.png new file mode 100644 index 0000000..5a0f90f Binary files /dev/null and b/images/square.png differ diff --git a/images/star.png b/images/star.png new file mode 100644 index 0000000..555fe91 Binary files /dev/null and b/images/star.png differ diff --git a/images/sun-o.png b/images/sun-o.png new file mode 100644 index 0000000..07a4eb5 Binary files /dev/null and b/images/sun-o.png differ diff --git a/images/tree.png b/images/tree.png new file mode 100644 index 0000000..f34aeb6 Binary files /dev/null and b/images/tree.png differ diff --git a/images/truck.png b/images/truck.png new file mode 100644 index 0000000..835d32e Binary files /dev/null and b/images/truck.png differ diff --git a/images/umbrella.png b/images/umbrella.png new file mode 100644 index 0000000..b9ba0ae Binary files /dev/null and b/images/umbrella.png differ diff --git a/index.php b/index.php new file mode 100644 index 0000000..207fe1c --- /dev/null +++ b/index.php @@ -0,0 +1,2 @@ + + + org.netbeans.modules.php.project + + + Captcheck + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9618f53 --- /dev/null +++ b/readme.md @@ -0,0 +1,60 @@ +Captcheck +========= + +Easy, light, self-hostable CAPTCHA service. Works on all modern browsers and +IE9+. Uses icons from Font-Awesome. + +How to use +---------- + +In your form, put an empty div with the ID "captcheck_container". +Add `captcheck.js` into your page. + + + + + Captcheck Sample Form + + + + + +
+ +
+
+ +
+ + + +When the form is submitted, your server will receive two extra form fields: +`captcheck_session_code` and `captcheck_selected_answer`. +In your form handling code, send a request to `http(s)://captcheck-url/api.php`. +Pass the variables `session_id` and `answer_id` with the values sent with the form, +and also pass the variable `action` with the value `verify`. +You will receive a JSON response with (among other things) `"result": true` or +`"result": false`. If result is false, the user failed the test, and another +variable `msg` is available with an explanation. + +Example URL: +`http(s)://captcheck-url/api.php?action=verify&session_id=&answer_id=` + +Example responses: +`{"session":"some_session_id","result":true}` +`{"session":"some_session_id","result":false,"msg":"Answer incorrect."}` + + +Execution Flow +-------------- + + JS = captcheck.js, API = api.php, FORM = parent form, + SITE = form processing code, -> = some action taken on the right by the left + + JS -> API: Request session ID, question, and answers (with scrambled random codes) + API -> JS: Sends info, saves session ID, correct answer, and scrambled answer codes in DB + JS -> API: Requests answer images by sending scrambled value and session ID + JS -> FORM: Adds hidden field with value=session ID, displays question and images + [USER SUBMITS FORM] + SITE -> API: Sends session ID and scrambled answer + API -> SITE: Responds with true/false to indicate if the answer is valid, marks session as expired to prevent CAPTCHA reuse diff --git a/required.php b/required.php new file mode 100644 index 0000000..557625e --- /dev/null +++ b/required.php @@ -0,0 +1,144 @@ + $error, "code" => $code])); +} + +// Database settings +// Also inits database and stuff +use Medoo\Medoo; + +$database; +try { + $database = new Medoo([ + 'database_type' => DB_TYPE, + 'database_name' => DB_NAME, + 'server' => DB_SERVER, + 'username' => DB_USER, + 'password' => DB_PASS, + 'charset' => DB_CHARSET + ]); +} catch (Exception $ex) { + sendError("Database error. Try again later."); +} + + +if (!DEBUG) { + error_reporting(0); +} else { + error_reporting(E_ALL); + ini_set('display_errors', 'On'); +} + + +$VARS; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $VARS = $_POST; + define("GET", false); +} else { + $VARS = $_GET; + define("GET", true); +} + +/** + * Checks if a string or whatever is empty. + * @param $str The thingy to check + * @return boolean True if it's empty or whatever. + */ +function is_empty($str) { + return (is_null($str) || !isset($str) || $str == ''); +} + +/* + * http://stackoverflow.com/a/20075147/2534036 + */ +if (!function_exists('base_url')) { + + function base_url($atRoot = FALSE, $atCore = FALSE, $parse = FALSE) { + if (isset($_SERVER['HTTP_HOST'])) { + $http = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off' ? 'https' : 'http'; + $hostname = $_SERVER['HTTP_HOST']; + $dir = str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']); + + $core = preg_split('@/@', str_replace($_SERVER['DOCUMENT_ROOT'], '', realpath(dirname(__FILE__))), NULL, PREG_SPLIT_NO_EMPTY); + $core = $core[0]; + + $tmplt = $atRoot ? ($atCore ? "%s://%s/%s/" : "%s://%s/") : ($atCore ? "%s://%s/%s/" : "%s://%s%s"); + $end = $atRoot ? ($atCore ? $core : $hostname) : ($atCore ? $core : $dir); + $base_url = sprintf($tmplt, $http, $hostname, $end); + } else + $base_url = 'http://localhost/'; + + if ($parse) { + $base_url = parse_url($base_url); + if (isset($base_url['path'])) + if ($base_url['path'] == '/') + $base_url['path'] = ''; + } + + return $base_url; + } + +} + + +/** + * Attempts to discover the user's IP address. + * @return string IP string or "NOT FOUND". + */ +function getUserIP() { + $ip = ""; + if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) { + $ip = $_SERVER["HTTP_CF_CONNECTING_IP"]; + } else if (isset($_SERVER["HTTP_CLIENT_IP"])) { + $ip = $_SERVER["HTTP_CLIENT_IP"]; + } else if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) { + $ip = $_SERVER["HTTP_X_FORWARDED_FOR"]; + } else if (isset($_SERVER["HTTP_X_FORWARDED"])) { + $ip = $_SERVER["HTTP_X_FORWARDED"]; + } else if (isset($_SERVER["HTTP_FORWARDED_FOR"])) { + $ip = $_SERVER["HTTP_FORWARDED_FOR"]; + } else if (isset($_SERVER["HTTP_FORWARDED"])) { + $ip = $_SERVER["HTTP_FORWARDED"]; + } else if (isset($_SERVER["REMOTE_ADDR"])) { + $ip = $_SERVER["REMOTE_ADDR"]; + } else { + $ip = "NOT FOUND"; + } + return $ip; +} \ No newline at end of file diff --git a/settings.template.php b/settings.template.php new file mode 100644 index 0000000..5653230 --- /dev/null +++ b/settings.template.php @@ -0,0 +1,14 @@ + + + + Captcheck Test Page + + + + + +
+ +
+
+ +
+ + diff --git a/test.php b/test.php new file mode 100644 index 0000000..6952c66 --- /dev/null +++ b/test.php @@ -0,0 +1,6 @@ + $_GET, "api" => json_decode(file_get_contents("http://localhost/captcheck/api.php?action=verify&session_id=" . $_GET["captcheck_session_code"] . "&answer_id=".$_GET["captcheck_selected_answer"]), true)]); \ No newline at end of file