From 8fafdc80a633a12040e812ec47727ceb9d9bad2b Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Sun, 16 May 2021 15:53:00 -0600 Subject: [PATCH] First commit, working framework and example API endpoints (/ping and /network/whois) --- .gitignore | 2 + .htaccess | 4 + apiconfig.php | 22 +++ authenticator.php | 16 ++ composer.json | 6 + composer.lock | 283 +++++++++++++++++++++++++++++++++++ endpoints/network.whois.php | 63 ++++++++ endpoints/ping.php | 9 ++ env.sample.php | 28 ++++ functions.php | 93 ++++++++++++ index.php | 109 ++++++++++++++ lib/Env.lib.php | 20 +++ lib/Logger.lib.php | 15 ++ lib/Memcache.lib.php | 93 ++++++++++++ nbproject/project.properties | 7 + nbproject/project.xml | 9 ++ required.php | 69 +++++++++ 17 files changed, 848 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 apiconfig.php create mode 100644 authenticator.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 endpoints/network.whois.php create mode 100644 endpoints/ping.php create mode 100644 env.sample.php create mode 100644 functions.php create mode 100644 index.php create mode 100644 lib/Env.lib.php create mode 100644 lib/Logger.lib.php create mode 100644 lib/Memcache.lib.php create mode 100644 nbproject/project.properties create mode 100644 nbproject/project.xml create mode 100644 required.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..716c0c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/env.php +/vendor/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..c26f042 --- /dev/null +++ b/.htaccess @@ -0,0 +1,4 @@ + + RewriteEngine on + RewriteRule ([a-zA-Z0-9]+) index.php?action=$1 [PT] + \ No newline at end of file diff --git a/apiconfig.php b/apiconfig.php new file mode 100644 index 0000000..1d2bdd0 --- /dev/null +++ b/apiconfig.php @@ -0,0 +1,22 @@ + [ + "load" => "ping.php", + "vars" => [ + ] + ], + "network/whois" => [ + "load" => "network.whois.php", + "vars" => [ + "domain" => "/\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b/", + "nocache (optional)" => "" + ] + ] +]; diff --git a/authenticator.php b/authenticator.php new file mode 100644 index 0000000..eb6675d --- /dev/null +++ b/authenticator.php @@ -0,0 +1,16 @@ +=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "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_pqsql": "For PostgreSQL database", + "ext-pdo_sqlite": "For SQLite database", + "ext-pdo_sqlsrv": "For MSSQL database on both Window/Liunx platform" + }, + "type": "framework", + "autoload": { + "psr-4": { + "Medoo\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Angel Lai", + "email": "angel@medoo.in" + } + ], + "description": "The lightweight PHP database framework to accelerate development", + "homepage": "https://medoo.in", + "keywords": [ + "database", + "database library", + "lightweight", + "mariadb", + "mssql", + "mysql", + "oracle", + "php framework", + "postgresql", + "sql", + "sqlite" + ], + "support": { + "issues": "https://github.com/catfan/Medoo/issues", + "source": "https://github.com/catfan/Medoo" + }, + "funding": [ + { + "url": "https://opencollective.com/medoo", + "type": "open_collective" + } + ], + "time": "2021-05-12T17:55:47+00:00" + }, + { + "name": "io-developer/php-whois", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/io-developer/php-whois.git", + "reference": "21a17d83fef83768c06fd7e1884f6898b0047b5d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/io-developer/php-whois/zipball/21a17d83fef83768c06fd7e1884f6898b0047b5d", + "reference": "21a17d83fef83768c06fd7e1884f6898b0047b5d", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=7.2", + "true/punycode": "^2.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Iodev\\": "src/Iodev/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sergey Sedyshev", + "email": "i.o.developer@gmail.com", + "homepage": "https://github.com/io-developer" + } + ], + "description": "PHP WHOIS provides parsed and raw whois lookup of domains and ASN routes. PHP 5.4+ and 7+ compatible ", + "homepage": "https://github.com/io-developer/php-whois", + "keywords": [ + "asn", + "domain", + "info", + "lookup", + "parser", + "php", + "query", + "routes", + "tld", + "whois", + "црщшы" + ], + "support": { + "issues": "https://github.com/io-developer/php-whois/issues", + "source": "https://github.com/io-developer/php-whois/tree/4.0.0" + }, + "time": "2020-03-22T21:36:05+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.22-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-01-22T09:19:47+00:00" + }, + { + "name": "true/punycode", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/true/php-punycode.git", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/true/php-punycode/zipball/a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "reference": "a4d0c11a36dd7f4e7cd7096076cab6d3378a071e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TrueBV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Renan Gonçalves", + "email": "renan.saddam@gmail.com" + } + ], + "description": "A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)", + "homepage": "https://github.com/true/php-punycode", + "keywords": [ + "idna", + "punycode" + ], + "support": { + "issues": "https://github.com/true/php-punycode/issues", + "source": "https://github.com/true/php-punycode/tree/master" + }, + "time": "2016-11-16T10:37:54+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/endpoints/network.whois.php b/endpoints/network.whois.php new file mode 100644 index 0000000..940b3c8 --- /dev/null +++ b/endpoints/network.whois.php @@ -0,0 +1,63 @@ +get("network.whois.$domain"); + if ($cacheresp !== false) { + exitWithJson(json_decode($cacheresp, true)); + } +} +try { + if ($memcache->enabled() && empty($VARS["nocache"])) { + $loader = new MemcachedLoader(new SocketLoader(), $memcache->getMemcached(), $memcache->getPrefix()); + $whois = Factory::get()->createWhois($loader); + } else { + $whois = Factory::get()->createWhois(); + } + $info = $whois->loadDomainInfo($domain); + $response = $whois->lookupDomain($domain); + if (!$info) { + $json = [ + "domain" => $domain, + "registered" => false + ]; + } else { + $json = [ + "domain" => $info->domainName, + "registered" => true, + "created" => $info->creationDate, + "expires" => $info->expirationDate, + "updated" => $info->updatedDate, + "registrar" => $info->registrar, + "owner" => $info->owner, + "nameservers" => $info->nameServers, + "raw" => $response->text + ]; + } + $json["status"] = "OK"; + $memcache->set("network.whois.$domain", json_encode($json), 60 * 60 * 4); + exitWithJson($json); +} catch (ConnectionException $e) { + sendJsonResp("Disconnect or connection timeout", "ERROR"); +} catch (ServerMismatchException $e) { + sendJsonResp("TLD server not found in current server hosts", "ERROR"); +} catch (WhoisException $e) { + sendJsonResp("Whois server responded with error '{$e->getMessage()}'", "ERROR"); +} catch (Exception $e) { + Logger::log("network.whois error: " . $e->getMessage()); + sendJsonResp("An unknown error occurred.", "ERROR"); +} \ No newline at end of file diff --git a/endpoints/ping.php b/endpoints/ping.php new file mode 100644 index 0000000..c764967 --- /dev/null +++ b/endpoints/ping.php @@ -0,0 +1,9 @@ + true, + "database" => [ + "database_type" => "mysql", + "database_name" => "mysql", + "server" => "localhost", + "username" => "root", + "password" => "", + "charset" => "utf8" + ], + "memcached" => [ + "enable" => true, + "server" => "127.0.0.1", + "port" => 11211, + "prefix" => "apiserver" + ], + // If this is running in a subfolder, set it accordingly or things will be strange + // Include trailing slash + "urlbase" => "/", +]; \ No newline at end of file diff --git a/functions.php b/functions.php new file mode 100644 index 0000000..a233ad9 --- /dev/null +++ b/functions.php @@ -0,0 +1,93 @@ + $val) { + if (strpos($key, "OR") === 0) { + checkVars($vars[$key], true); + continue; + } + + // Only check type of optional variables if they're set, and don't + // mark them as bad if they're not set + if (strpos($key, " (optional)") !== false) { + $key = str_replace(" (optional)", "", $key); + if (empty($VARS[$key])) { + continue; + } + } else { + if (empty($VARS[$key])) { + $ok[$key] = false; + continue; + } + } + + // If there's no pattern at all + if (empty($val)) { + $ok[$key] = true; + continue; + } + + if (strpos($val, "/") === 0) { + // regex + $ok[$key] = preg_match($val, $VARS[$key]) === 1; + } else { + $checkmethod = "is_$val"; + $ok[$key] = !($checkmethod($VARS[$key]) !== true); + } + } + if ($or) { + $success = false; + $bad = ""; + foreach ($ok as $k => $v) { + if ($v) { + $success = true; + break; + } else { + $bad = $k; + } + } + if (!$success) { + http_response_code(400); + die("400 Bad request: variable $bad is missing or invalid"); + } + } else { + foreach ($ok as $key => $bool) { + if (!$bool) { + http_response_code(400); + die("400 Bad request: variable $key is missing or invalid"); + } + } + } +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..dc41611 --- /dev/null +++ b/index.php @@ -0,0 +1,109 @@ += 1) { + $ep = $route[0]; + // Ignore URL parameters if present, + // otherwise requests like /endpoint?abc=123 will have the endpoint "endpoint?abc=123" + $ep = explode("?", $ep, 2)[0]; + // Don't allow = in endpoint ID because it's confusing + if (strpos($ep, "=") === FALSE) { + $ENDPOINT = $ep; + } +} +$urlkeyvaluepairs = []; +if (count($route) >= 2 && strpos($route[1], "?") !== 0) { + for ($i = 1; $i < count($route); $i++) { + if (empty($route[$i])) { + continue; + } + if (strpos($route[$i], "=") === false) { + // Allow slashes in endpoint as long as no key/value pairs have + // been found yet + if (empty($urlkeyvaluepairs)) { + $ENDPOINT .= "/" . $route[$i]; + } + continue; + } + $key = explode("=", $route[$i], 2)[0]; + $val = explode("=", $route[$i], 2)[1]; + $urlkeyvaluepairs[$key] = $val; + } +} + +$VARS += $urlkeyvaluepairs; + +// Deny access if authenticator returns false (add your logic to authenticator.php) +if (!authenticaterequest()) { + http_response_code(401); + die("401 Unauthorized: You need to supply valid credentials."); +} + +if (empty($ENDPOINT)) { + http_response_code(404); + die("404 No endpoint specified."); +} + +if (!isset($APIS[$ENDPOINT])) { + http_response_code(404); + die("404 Requested endpoint not known."); +} + +$APIENDPOINTCONFIG = $APIS[$ENDPOINT]; + +if (!file_exists(__DIR__ . "/endpoints/" . $APIENDPOINTCONFIG["load"])) { + http_response_code(404); + die("404 Requested endpoint known but not found."); +} + +// Check and validate arguments/data passed from client based on configured patterns +// This makes sure that all required variables are present and not malformed, reducing +// the amount of validation code in each endpoint +if (!empty($APIENDPOINTCONFIG["vars"])) { + checkVars($APIENDPOINTCONFIG["vars"]); +} + +// cleanup variables that won't be needed in endpoints +unset($route, $ep, $key, $val, $urlkeyvaluepairs, $pos, $requestjson, $requestbody, $i, $routestr); + +require_once __DIR__ . "/endpoints/" . $APIENDPOINTCONFIG["load"]; diff --git a/lib/Env.lib.php b/lib/Env.lib.php new file mode 100644 index 0000000..d922c18 --- /dev/null +++ b/lib/Env.lib.php @@ -0,0 +1,20 @@ +enabled = $enabled; + $this->server = $server; + $this->port = $port; + $this->prefix = $prefix; + if ($enabled) { + $this->memcache = new Memcached(); + $this->memcache->addServer($this->server, $this->port); + } + $this->dummycache = []; + } + + /** + * Check if the memcache is actually setup or if it's a dummy shim. + * @return bool true if memcache is enabled, false if only dummy cache enabled + */ + function enabled(): bool { + return $this->enabled; + } + + /** + * Get the value for key. + * @param string $key + * @return value or false if not found + */ + function get(string $key) { + if ($this->enabled) { + $key = $this->prefix . $key; + return $this->memcache->get($key); + } else { + if (!empty($this->dummycache[$key])) { + return $this->dummycache[$key]; + } + return false; + } + } + + /** + * Set the value of $key to $val + * @param string $key + * @param type $val + * @param int $expires number of seconds (max 60*60*24*30) or UNIX timestamp for cache expiry + * Default expiration of 0 means no expiration + */ + function set(string $key, $val, int $expires = 0) { + if ($expires < 0) { + $expires = 0; + } + if ($this->enabled) { + $key = $this->prefix . $key; + $this->memcache->set($key, $val, $expires); + } else { + $this->dummycache[$key] = $val; + } + } + + /** + * Get the cache key prefix string + * @return string + */ + function getPrefix(): string { + return $this->prefix; + } + + function getMemcached() { + return $this->memcache; + } + +} diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 0000000..112ce7b --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,7 @@ +include.path=${php.global.include.path} +php.version=PHP_74 +source.encoding=UTF-8 +src.dir=. +tags.asp=false +tags.short=false +web.root=. diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 0000000..615db9d --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.php.project + + + apis.netsyms.net + + + diff --git a/required.php b/required.php new file mode 100644 index 0000000..59a0c93 --- /dev/null +++ b/required.php @@ -0,0 +1,69 @@ + false, + "server" => "127.0.0.1", + "port" => 11211, + "prefix" => "apiserver" + ]); +$memcache = new Memcache($memcacheconfig["enable"], $memcacheconfig["server"], $memcacheconfig["port"], $memcacheconfig["prefix"]); +unset($memcacheconfig); + +if (env("debugmode", false)) { + error_reporting(E_ALL); + ini_set('display_errors', 'On'); +} else { + error_reporting(0); + ini_set('display_errors', 'Off'); +} \ No newline at end of file