From adb24dae9f65d114230823c0155584abfd578b4d Mon Sep 17 00:00:00 2001 From: flashwave Date: Sun, 25 Aug 2024 01:50:52 +0000 Subject: [PATCH] Initial import. --- .gitattributes | 1 + .gitignore | 6 + LICENCE | 30 + README.md | 3 + aleister.php | 32 + composer.json | 16 + composer.lock | 852 ++++++++++++++++++++++++ phpstan.neon | 9 + public/index.php | 21 + public/robots.txt | 2 + src/AleisterContentHandler.php | 58 ++ src/AleisterContext.php | 92 +++ src/AleisterErrorHandler.php | 18 + src/HttpAcceptHeader.php | 59 ++ src/OAuth2/OAuth2Routes.php | 190 ++++++ src/RpcClientWrapper.php | 47 ++ src/RpcModels/Hanyuu/OAuth2AuthInfo.php | 38 ++ src/RpcModels/Hanyuu/OAuth2RfcModel.php | 22 + src/RpcModels/RpcModel.php | 55 ++ src/V1/V1Context.php | 20 + src/V1/V1Routes.php | 16 + 21 files changed, 1587 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 README.md create mode 100644 aleister.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 phpstan.neon create mode 100644 public/index.php create mode 100644 public/robots.txt create mode 100644 src/AleisterContentHandler.php create mode 100644 src/AleisterContext.php create mode 100644 src/AleisterErrorHandler.php create mode 100644 src/HttpAcceptHeader.php create mode 100644 src/OAuth2/OAuth2Routes.php create mode 100644 src/RpcClientWrapper.php create mode 100644 src/RpcModels/Hanyuu/OAuth2AuthInfo.php create mode 100644 src/RpcModels/Hanyuu/OAuth2RfcModel.php create mode 100644 src/RpcModels/RpcModel.php create mode 100644 src/V1/V1Context.php create mode 100644 src/V1/V1Routes.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb3a0bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +[Tt]humbs.db +[Dd]esktop.ini +.DS_Store +/.debug +/vendor +/aleister.cfg diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..78866df --- /dev/null +++ b/LICENCE @@ -0,0 +1,30 @@ +Copyright (c) 2024, flashwave +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted (subject to the limitations in the disclaimer +below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3ed708c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Aleister + +Aleister is the API gateway server for Flashii. diff --git a/aleister.php b/aleister.php new file mode 100644 index 0000000..786641a --- /dev/null +++ b/aleister.php @@ -0,0 +1,32 @@ +hasValues('sentry:dsn')) + (function($cfg) { + \Sentry\init([ + 'dsn' => $cfg->getString('dsn'), + 'traces_sample_rate' => $cfg->getFloat('tracesRate', 0.2), + 'profiles_sample_rate' => $cfg->getFloat('profilesRate', 0.2), + ]); + + set_exception_handler(function(\Throwable $ex) { + \Sentry\captureException($ex); + }); + })($cfg->scopeTo('sentry')); + +$ctx = new AleisterContext($cfg); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..721c6e9 --- /dev/null +++ b/composer.json @@ -0,0 +1,16 @@ +{ + "autoload": { + "psr-4": { + "Aleister\\": "src" + } + }, + "require": { + "flashwave/index": "^0.2408.40014", + "flashwave/syokuhou": "^1.2", + "flashwave/aiwass": "^1.0", + "sentry/sdk": "^4.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..d57ab95 --- /dev/null +++ b/composer.lock @@ -0,0 +1,852 @@ +{ + "_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#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "f16579c0b9bf3a7b6091a2eec61e2354", + "packages": [ + { + "name": "flashwave/aiwass", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://patchii.net/flashii/aiwass.git", + "reference": "de27da54b603f0fdc06dab89341908e73ea7a880" + }, + "require": { + "ext-msgpack": ">=2.2", + "flashwave/index": "^0.2408.40014", + "php": ">=8.3" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^11.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Aiwass\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "bsd-3-clause-clear" + ], + "authors": [ + { + "name": "flashwave", + "email": "packagist@flash.moe", + "homepage": "https://flash.moe", + "role": "mom" + } + ], + "description": "Shared HTTP RPC client/server library.", + "homepage": "https://railgun.sh/aiwass", + "time": "2024-08-16T15:59:19+00:00" + }, + { + "name": "flashwave/index", + "version": "v0.2408.182001", + "source": { + "type": "git", + "url": "https://patchii.net/flash/index.git", + "reference": "5a1fdcccedf818897a3468d5457875fabfb2ce28" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^11.2" + }, + "suggest": { + "ext-mysqli": "Support for the Index\\Data\\MariaDB namespace (both mysqlnd and libmysql are supported).", + "ext-sqlite3": "Support for the Index\\Data\\SQLite namespace." + }, + "type": "library", + "autoload": { + "psr-4": { + "Index\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "bsd-3-clause-clear" + ], + "authors": [ + { + "name": "flashwave", + "email": "packagist@flash.moe", + "homepage": "https://flash.moe", + "role": "mom" + } + ], + "description": "Composer package for the common library for my projects.", + "homepage": "https://railgun.sh/index", + "time": "2024-08-18T20:01:21+00:00" + }, + { + "name": "flashwave/syokuhou", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://patchii.net/flash/syokuhou.git", + "reference": "129a46c0d917382f9bc195cce278be51984eb87d" + }, + "require": { + "flashwave/index": "^0.2408.40014", + "php": ">=8.3" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^11.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Syokuhou\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "bsd-3-clause-clear" + ], + "authors": [ + { + "name": "flashwave", + "email": "packagist@flash.moe", + "homepage": "https://flash.moe", + "role": "mom" + } + ], + "description": "Configuration library for PHP.", + "homepage": "https://railgun.sh/syokuhou", + "time": "2024-08-04T01:07:23+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + }, + "time": "2024-03-08T09:58:59+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sentry/sdk", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php-sdk.git", + "reference": "fcbca864e8d1dc712f3ecfaa95ea89d024fb2e53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/fcbca864e8d1dc712f3ecfaa95ea89d024fb2e53", + "reference": "fcbca864e8d1dc712f3ecfaa95ea89d024fb2e53", + "shasum": "" + }, + "require": { + "sentry/sentry": "^4.0" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "This is a meta package of sentry/sentry. We recommend using sentry/sentry directly.", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php-sdk/issues", + "source": "https://github.com/getsentry/sentry-php-sdk/tree/4.0.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-11-06T10:23:19+00:00" + }, + { + "name": "sentry/sentry", + "version": "4.9.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/788ec170f51ebb22f2809a1e3f78b19ccd39b70d", + "reference": "788ec170f51ebb22f2809a1e3f78b19ccd39b70d", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^1.0|^2.0", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "monolog/monolog": "^1.6|^2.0|^3.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5.14|^9.4", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.9.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2024-08-08T14:40:50+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.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": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "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": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.1.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": "2024-05-31T14:57:53+00:00" + } + ], + "packages-dev": [ + { + "name": "phpstan/phpstan", + "version": "1.11.10", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "640410b32995914bde3eed26fa89552f9c2c082f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/640410b32995914bde3eed26fa89552f9c2c082f", + "reference": "640410b32995914bde3eed26fa89552f9c2c082f", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-08-08T09:02:50+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..d1e70dc --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,9 @@ +parameters: + level: 5 + paths: + - src + bootstrapFiles: + - aleister.php + dynamicConstantNames: + - CRW_CLI + - CRW_DEBUG diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..4738ca1 --- /dev/null +++ b/public/index.php @@ -0,0 +1,21 @@ +getRouter()->dispatch(); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/src/AleisterContentHandler.php b/src/AleisterContentHandler.php new file mode 100644 index 0000000..555b850 --- /dev/null +++ b/src/AleisterContentHandler.php @@ -0,0 +1,58 @@ +accept->getMostPreferred([ + 'application/json', + 'text/html', // pretty printed JSON + ]) !== null; + } + + public function match(mixed $content): bool { + return true; + } + + public function handle(HttpResponseBuilder $response, mixed $content): void { + // mostly for debug and info pages, so we'll ignore the accept header for strings + if(is_string($content)) { + if(stripos($content, 'setTypeHTML('utf-8'); + else + $response->setTypePlain('utf-8'); + + $response->setContent($content); + return; + } + + if($content instanceof JsonSerializable) + $content = $content->jsonSerialize(); + elseif($content instanceof IBencodeSerialisable) + $content = $content->bencodeSerialise(); + + // default to JSON + if(!$response->hasContentType()) + $response->setTypeJson(); + + $flags = JSON_UNESCAPED_UNICODE + | JSON_UNESCAPED_SLASHES + | JSON_THROW_ON_ERROR + | JSON_PRESERVE_ZERO_FRACTION + | JSON_INVALID_UTF8_SUBSTITUTE; + + // pretty print if text/html is acceptable, someone is likely poking using a browser + if($this->accept->isAcceptable('text/html')) + $flags |= JSON_PRETTY_PRINT; + + $response->setContent(json_encode($content, $flags)); + } +} diff --git a/src/AleisterContext.php b/src/AleisterContext.php new file mode 100644 index 0000000..0f23ebe --- /dev/null +++ b/src/AleisterContext.php @@ -0,0 +1,92 @@ +authInfo = new OAuth2AuthInfo(['error' => 'none']); + + $this->rpcWrapper = new RpcClientWrapper; + $this->rpcWrapper->createHmacConfig($config, 'hanyuu'); + + $this->router = new HttpRouter( + errorHandler: 'plain', + registerDefaultContentHandlers: false + ); + $this->router->use('/', fn($resp) => $resp->setPoweredBy('Flashii')); + $this->router->use('/', $this->handleAcceptHeader(...)); + $this->router->use('/', $this->handleAuthzHeader(...)); + $this->router->get('/', fn() => 'Hello! Someday this page will probably redirect to documentation, but none exists yet.'); + + $this->router->scopeTo('/oauth2')->register(new OAuth2\OAuth2Routes($this)); + $this->router->scopeTo('/v1')->register(new V1\V1Routes(new V1\V1Context($this))); + } + + public function getRpcClient(): RpcClientWrapper { + return $this->rpcWrapper; + } + + public function getRouter(): IRouter { + return $this->router; + } + + public function getAuthInfo(): OAuth2AuthInfo { + return $this->authInfo; + } + + public function handleAcceptHeader($response, $request) { + $accept = HttpAcceptHeader::parse($request->getHeaderLine('Accept')); + $contentHandler = new AleisterContentHandler($accept); + + if(!$contentHandler->hasAcceptableType()) + return 406; + + $this->router->setErrorHandler(new AleisterErrorHandler($contentHandler)); + $this->router->registerContentHandler($contentHandler); + } + + public function handleAuthzHeader($response, $request): void { + $authInfo = null; + + $authzHeader = explode(' ', (string)$request->getHeaderLine('Authorization'), 2); + if(count($authzHeader) > 1) { + $authzMethod = array_shift($authzHeader); + $authzInfo = array_shift($authzHeader); + + if(strcasecmp($authzMethod, 'basic') === 0) { + $authzInfo = base64_decode($authzInfo); + if($authzInfo !== false) { + $authzInfo = explode(':', $authzInfo, 2); + if(count($authzInfo) > 0) + try { + $authInfo = $this->rpcWrapper->procedure( + 'hanyuu:oauth2:attemptAppAuth', + ['clientId' => array_shift($authzInfo), 'clientSecret' => array_shift($authzInfo) ?? ''] + ); + } catch(RuntimeException $ex) {} + } + } elseif(strcasecmp($authzMethod, 'bearer') === 0) { + try { + $authInfo = $this->rpcWrapper->procedure( + 'hanyuu:oauth2:getTokenInfo', + ['type' => 'Bearer', 'token' => $authzInfo] + ); + } catch(RuntimeException $ex) {} + } + } + + if($authInfo !== null) + $this->authInfo = new OAuth2AuthInfo($authInfo); + } +} diff --git a/src/AleisterErrorHandler.php b/src/AleisterErrorHandler.php new file mode 100644 index 0000000..06d56fc --- /dev/null +++ b/src/AleisterErrorHandler.php @@ -0,0 +1,18 @@ +contentHandler->handle($response, [ + 'code' => sprintf('http:%s', $code), + 'message' => $message, + ]); + } +} diff --git a/src/HttpAcceptHeader.php b/src/HttpAcceptHeader.php new file mode 100644 index 0000000..3144adf --- /dev/null +++ b/src/HttpAcceptHeader.php @@ -0,0 +1,59 @@ +accept, fn($a, $b) => $b->getQuality() <=> $a->getQuality()); + } + + public function isAcceptable(string $option, bool $strict = false): bool { + if(empty($this->accept)) + return true; + + foreach($this->accept as $type) + if($type->equals($option) && $type->getQuality() > 0 + && (!$strict || ($type->getCategory() !== '*' && $type->getKind() !== '*'))) + return true; + + return false; + } + + public function getMostPreferred(array $options = []): ?string { + if(empty($options)) + return null; + if(empty($this->accept)) + return $options[array_key_first($options)]; + + $prefer = null; + $weight = 0; + + foreach($options as $option) + foreach($this->accept as $type) { + $quality = $type->getQuality(); + if($quality <= 0 || $quality <= $weight || !$type->equals($option)) + continue; + + $prefer = $option; + $weight = $quality; + } + + return $prefer; + } + + public static function parse(string $header): HttpAcceptHeader { + $accept = []; + $types = explode(',', $header); + + foreach($types as $type) { + $type = trim($type); + if($type !== '') + $accept[] = MediaType::parse($type); + } + + return new HttpAcceptHeader($accept); + } +} diff --git a/src/OAuth2/OAuth2Routes.php b/src/OAuth2/OAuth2Routes.php new file mode 100644 index 0000000..2df027b --- /dev/null +++ b/src/OAuth2/OAuth2Routes.php @@ -0,0 +1,190 @@ + $value) + $wwwAuth .= sprintf(', %s="%s"', $name, rawurlencode($value)); + + $response->setStatusCode(401); + $response->setHeader('WWW-Authenticate', $wwwAuth); + } else + $response->setStatusCode(400); + } + + return $result; + } + + #[HttpPost('/request-authorise')] + public function postRequestAuthorise($response, $request) { + $response->setHeader('Cache-Control', 'no-store'); + + $authInfo = $this->ctx->getAuthInfo(); + if($authInfo->getError() === 'secret') + return self::filter($response, [ + 'error' => 'invalid_client', + 'error_description' => 'Provided client secret is not correct for this application.', + ], authzHeader: true); + + if(!$request->isFormContent()) + return self::filter($response, [ + 'error' => 'invalid_request', + 'error_description' => 'Your request must use content type application/x-www-form-urlencoded.', + ]); + + $rpc = $this->ctx->getRpcClient(); + $content = $request->getContent(); + + if($authInfo->getMethod() === '') + try { + $authInfo = new OAuth2AuthInfo($rpc->procedure('hanyuu:oauth2:attemptAppAuth', [ + 'clientId' => (string)$content->getParam('client_id'), + ])); + } catch(RuntimeException $ex) {} + + if(!$authInfo->isAppUser()) + return self::filter($response, [ + 'error' => 'invalid_client', + 'error_description' => 'App authentication failed.', + ]); + + try { + $reqInfo = new OAuth2RfcModel($rpc->procedure('hanyuu:oauth2:createAuthoriseRequest', [ + 'appId' => $authInfo->getAppId(), + 'scope' => (string)$content->getParam('scope'), + ])); + } catch(RuntimeException $ex) {} + + if($reqInfo === null || ($reqInfo->hasError() && !in_array($reqInfo->getError(), self::REQ_AUTH_ERRORS))) + return self::filter($response, [ + 'error' => 'server_error', + 'error_description' => 'Authorisation server gave unexpected response.', + ]); + + return self::filter($response, $reqInfo->getRaw()); + } + + #[HttpOptions('/token')] + #[HttpPost('/token')] + public function postToken($response, $request) { + $response->setHeader('Cache-Control', 'no-store'); + + $originHeaders = ['Origin', 'X-Origin', 'Referer']; + $origins = []; + foreach($originHeaders as $originHeader) { + $originHeader = $request->getHeaderFirstLine($originHeader); + if($originHeader !== '' && !in_array($originHeader, $origins)) + $origins[] = $originHeader; + } + + if(!empty($origins)) { + // TODO: check if none of the provided origins is on a blocklist or something + // different origins being specified for each header should probably also be considered suspect... + + $response->setHeader('Access-Control-Allow-Origin', $origins[0]); + $response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST'); + $response->setHeader('Access-Control-Allow-Headers', 'Authorization'); + $response->setHeader('Access-Control-Expose-Headers', 'Vary'); + foreach($originHeaders as $originHeader) + $response->setHeader('Vary', $originHeader); + } + + if($request->getMethod() === 'OPTIONS') + return 204; + + if(!$request->isFormContent()) + return self::filter($response, [ + 'error' => 'invalid_request', + 'error_description' => 'Your request must use content type application/x-www-form-urlencoded.', + ]); + + $rpc = $this->ctx->getRpcClient(); + $content = $request->getContent(); + + $authInfo = $this->ctx->getAuthInfo(); + if($authInfo->getMethod() === '') + try { + $authInfo = new OAuth2AuthInfo($rpc->procedure('hanyuu:oauth2:attemptAppAuth', [ + 'clientId' => (string)$content->getParam('client_id'), + 'clientSecret' => (string)$content->getParam('client_secret') + ])); + } catch(RuntimeException $ex) { + $authInfo = null; + } + + if($authInfo === null || !$authInfo->isAppUser()) + return self::filter($response, [ + 'error' => 'invalid_client', + 'error_description' => 'App authentication failed.', + ]); + if($authInfo->getError() === 'secret') + return self::filter($response, [ + 'error' => 'invalid_client', + 'error_description' => 'Provided client secret is not correct for this application.' + ], authzHeader: true); + + try { + $name = ''; + $args = ['appId' => $authInfo->getAppId(), 'isAuthed' => $authInfo->isAuthed()]; + + switch($content->getParam('grant_type')) { + case 'authorization_code': + $name = 'authorisationCode'; + $args['code'] = (string)$content->getParam('code'); + $args['codeVerifier'] = (string)$content->getParam('code_verifier'); + break; + + case 'refresh_token': + $name = 'refreshToken'; + $args['refreshToken'] = (string)$content->getParam('refresh_token'); + + if($content->hasParam('scope')) + $args['scope'] = (string)$content->getParam('scope'); + break; + + case 'client_credentials': + $name = 'clientCredentials'; + + if($content->hasParam('scope')) + $args['scope'] = (string)$content->getParam('scope'); + break; + + case 'urn:ietf:params:oauth:grant-type:device_code': + case 'device_code': + $name = 'deviceCode'; + $args['deviceCode'] = (string)$content->getParam('device_code'); + break; + } + + if($name === '') + return self::filter($response, [ + 'error' => 'unsupported_grant_type', + 'error_description' => 'Requested grant type is not supported by this server.', + ]); + + return self::filter($response, $rpc->procedure('hanyuu:oauth2:createBearerToken:' . $name, $args)); + } catch(RuntimeException $ex) { + return self::filter($response, [ + 'error' => 'server_error', + 'error_description' => 'Authorisation server gave unexpected response.', + ]); + } + } +} diff --git a/src/RpcClientWrapper.php b/src/RpcClientWrapper.php new file mode 100644 index 0000000..c1ce97d --- /dev/null +++ b/src/RpcClientWrapper.php @@ -0,0 +1,47 @@ +scopeTo($prefix); + $this->createHmac( + $prefix . ':', + $config->getString('endpoint'), + fn() => $config->getString('secret') + ); + } + + public function createHmac(string $prefix, string $url, $getSecretKey): void { + $this->register($prefix, RpcClient::createHmac($url, $getSecretKey)); + } + + public function register(string $prefix, IRpcClient $client): void { + $this->clients[$prefix] = $client; + } + + public function scopeTo(string $prefix): IRpcClient { + return new RpcClientScoped($this, $prefix); + } + + public function query(string $action, array $params): mixed { + foreach($this->clients as $prefix => $client) + if(str_starts_with($action, $prefix)) + return $client->query($action, $params); + + throw new RuntimeException(sprintf('Unable to handle this query: %s', $action)); + } + + public function procedure(string $action, array $params): mixed { + foreach($this->clients as $prefix => $client) + if(str_starts_with($action, $prefix)) + return $client->procedure($action, $params); + + throw new RuntimeException(sprintf('Unable to handle this procedure: %s', $action)); + } +} diff --git a/src/RpcModels/Hanyuu/OAuth2AuthInfo.php b/src/RpcModels/Hanyuu/OAuth2AuthInfo.php new file mode 100644 index 0000000..a7df553 --- /dev/null +++ b/src/RpcModels/Hanyuu/OAuth2AuthInfo.php @@ -0,0 +1,38 @@ +getString('method'); + } + + public function isAuthed(): bool { + return $this->getBoolean('authed'); + } + + public function getAppId(): string { + return $this->getString('app_id'); + } + + public function hasUserId(): bool { + return $this->hasValue('user_id'); + } + + public function isAppUser(): bool { + return $this->getMethod() === 'basic' || $this->getUserId() === '0'; + } + + public function getUserId(): string { + return $this->getString('user_id'); + } + + public function getScope(): string { + return $this->getString('scope'); + } + + public function getExpiresIn(): int { + return $this->getInteger('expires_in'); + } +} diff --git a/src/RpcModels/Hanyuu/OAuth2RfcModel.php b/src/RpcModels/Hanyuu/OAuth2RfcModel.php new file mode 100644 index 0000000..f9b342d --- /dev/null +++ b/src/RpcModels/Hanyuu/OAuth2RfcModel.php @@ -0,0 +1,22 @@ +hasValue('error_description'); + } + + public function getErrorDescription(): string { + return $this->getString('error_description'); + } + + public function hasErrorUri(): bool { + return $this->hasValue('error_uri'); + } + + public function getErrorUri(): string { + return $this->getString('error_uri'); + } +} diff --git a/src/RpcModels/RpcModel.php b/src/RpcModels/RpcModel.php new file mode 100644 index 0000000..1903942 --- /dev/null +++ b/src/RpcModels/RpcModel.php @@ -0,0 +1,55 @@ +info = $info; + } + + public function hasError(): bool { + return $this->hasValue('error'); + } + + public function getError(): string { + return $this->getString('error'); + } + + public function getRaw(): array { + return $this->info; + } + + protected function hasValue(string $name): bool { + return array_key_exists($name, $this->info); + } + + protected function getValue(string $name, mixed $fallback = null): mixed { + return array_key_exists($name, $this->info) ? $this->info[$name] : $fallback; + } + + protected function getArray(string $name): array { + return array_key_exists($name, $this->info) && is_array($this->info[$name]) + ? $this->info[$name] : []; + } + + protected function getBoolean(string $name): bool { + return array_key_exists($name, $this->info) && is_bool($this->info[$name]) + && $this->info[$name]; + } + + protected function getInteger(string $name): int { + return array_key_exists($name, $this->info) && is_int($this->info[$name]) + ? $this->info[$name] : 0; + } + + protected function getFloat(string $name): float { + return array_key_exists($name, $this->info) && is_float($this->info[$name]) + ? $this->info[$name] : 0.0; + } + + protected function getString(string $name): string { + return array_key_exists($name, $this->info) && is_string($this->info[$name]) + ? $this->info[$name] : ''; + } +} diff --git a/src/V1/V1Context.php b/src/V1/V1Context.php new file mode 100644 index 0000000..faa4caf --- /dev/null +++ b/src/V1/V1Context.php @@ -0,0 +1,20 @@ +ctx->getRpcClient(); + } + + public function getAuthInfo(): OAuth2AuthInfo { + return $this->ctx->getAuthInfo(); + } +} diff --git a/src/V1/V1Routes.php b/src/V1/V1Routes.php new file mode 100644 index 0000000..ec8db88 --- /dev/null +++ b/src/V1/V1Routes.php @@ -0,0 +1,16 @@ +get('/', fn() => ['status' => 'operational']); + } +}