Initial commit

This commit is contained in:
Skylar Ittner 2017-06-09 03:33:56 -06:00
commit 563399081b
45 changed files with 503 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
nbproject/private
settings.php
database.mwb.bak
debug
vendor

9
LICENSE Normal file
View File

@ -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.

90
api.php Normal file
View File

@ -0,0 +1,90 @@
<?php
require __DIR__ . '/required.php';
header("Content-Type: application/json");
switch ($VARS['action']) {
case "ping":
$out = ["status" => "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");
}

58
captcheck.js Normal file
View File

@ -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 += "<span class='captcheck_answer_label' onclick='chooseAnswer(\"" + data.answers[i] + "\")'><input id='captcheck_answer_" + data.answers[i] + "' type='radio' name='captcheck_selected_answer' value='" + data.answers[i] + "' /><img src='" + src + "' /></span>";
}
var answer_div = document.createElement("div");
answer_div.innerHTML = answers;
/* Create question */
var question_div = document.createElement("div");
question_div.innerHTML = "Click on the <b>" + data.question + "</b>:";
/* 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 = "<input type='hidden' name='captcheck_session_code' value='" + data.session + "' />";
captcha.appendChild(skey_input);
} else {
/* Add error message */
captcha.innerHTML = "<span class='captcheck_error_message'>There was a problem loading the CAPTCHA.</span>";
}
});
}
function chooseAnswer(ans) {
var box = document.getElementById("captcheck_answer_" + ans);
box.checked = true;
}

5
composer.json Normal file
View File

@ -0,0 +1,5 @@
{
"require": {
"catfan/medoo": "^1.4"
}
}

77
composer.lock generated Normal file
View File

@ -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": []
}

BIN
database.mwb Normal file

Binary file not shown.

BIN
images/bolt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

BIN
images/building.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

BIN
images/camera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

BIN
images/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 B

BIN
images/cloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
images/cog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

BIN
images/cube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

BIN
images/envelope.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

BIN
images/female.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

BIN
images/file-o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

BIN
images/flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

BIN
images/globe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/heart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

BIN
images/male.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 B

BIN
images/mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

BIN
images/moon-o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/paint-brush.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

BIN
images/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

BIN
images/picture-o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

BIN
images/plane.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

BIN
images/print.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

BIN
images/puzzle-piece.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

BIN
images/shopping-basket.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

BIN
images/snowflake-o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
images/square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

BIN
images/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B

BIN
images/sun-o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

BIN
images/tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

BIN
images/truck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

BIN
images/umbrella.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

2
index.php Normal file
View File

@ -0,0 +1,2 @@
<?php
header("Location: test.html");

View File

@ -0,0 +1,7 @@
include.path=${php.global.include.path}
php.version=PHP_70
source.encoding=UTF-8
src.dir=.
tags.asp=false
tags.short=false
web.root=.

9
nbproject/project.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.php.project</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/php-project/1">
<name>Captcheck</name>
</data>
</configuration>
</project>

60
readme.md Normal file
View File

@ -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.
<!DOCTYPE html>
<html>
<head>
<title>Captcheck Sample Form</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="captcheck.js"></script>
</head>
<body>
<form action="submit.php">
<input type="text" name="form_field" placeholder="Some random form field" />
<div id="captcheck_container">
</div>
<button type="submit">Submit Form</button>
</form>
</body>
</html>
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=<captcheck_session_code>&answer_id=<captcheck_selected_answer>`
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

144
required.php Normal file
View File

@ -0,0 +1,144 @@
<?php
/**
* This file contains global settings and utility functions.
*/
ob_start(); // allow sending headers after content
//
// Unicode, solves almost all stupid encoding problems
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Access-Control-Allow-Origin: *');
$session_length = (60 * 60) * 8; // 1 hour x 8 = 8 hours
session_set_cookie_params($session_length, "/", null, false, true);
session_start(); // stick some cookies in it
// renew session cookie
setcookie(session_name(), session_id(), time() + $session_length);
// Composer
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/settings.php';
/**
* Kill off the running process and spit out an error message
* @param string $error error message
* @param string $fault who's fault the error is: "server" (default) or "client".
*/
function sendError($error, $fault = "server") {
if ($fault == "server") {
header('HTTP/1.1 500 Internal Server Error');
$code = 500;
} else {
header('HTTP/1.1 400 Bad Request');
$code = 400;
}
die(json_encode(["error" => $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;
}

14
settings.template.php Normal file
View File

@ -0,0 +1,14 @@
<?php
// Whether to show debugging data in output.
// DO NOT SET TO TRUE IN PRODUCTION!!!
define("DEBUG", false);
// Database connection settings
// See http://medoo.in/api/new for info
define("DB_TYPE", "mysql");
define("DB_NAME", "captcheck");
define("DB_SERVER", "localhost");
define("DB_USER", "");
define("DB_PASS", "");
define("DB_CHARSET", "utf8");

17
test.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>Captcheck Test Page</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="captcheck.js"></script>
</head>
<body>
<form action="test.php" method="GET">
<input type="text" name="junk" placeholder="Junk" />
<div id="captcheck_container">
</div>
<button type="submit">Submit Form</button>
</form>
</body>
</html>

6
test.php Normal file
View File

@ -0,0 +1,6 @@
<?php
//header("Content-Type: application/json");
header("Content-Type: text/plain");
echo json_encode(["get" => $_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)]);