diff --git a/README.md b/README.md index d8a1729..4689dee 100644 --- a/README.md +++ b/README.md @@ -16,18 +16,17 @@ _Standalone Solid Server written in PHP by PDS Interop_ - [Background](#background) - * [Installation](#installation) +- [Installation](#installation) - [Usage](#usage) - * [Docker images](#docker-images) - * [Local environment](#local-environment) - + [Built-in PHP HTTP server](#built-in-php-http-server) + - [Docker images](#docker-images) + - [Local environment](#local-environment) + - [Built-in PHP HTTP server](#built-in-php-http-server) - [Security](#security) - [Running solid/webid-provider-tests](#running-solidwebid-provider-tests) - [Available Features](#available-features) - [Development](#development) - * [Project structure](#project-structure) - * [Coding conventions](#coding-conventions) - * [Testing](#testing) + - [Project structure](#project-structure) + - [Testing](#testing) - [Contributing](#contributing) - [License](#license) @@ -47,10 +46,6 @@ they define: - Resource (reading and writing) API - Social Web App Protocols (Notifications, Friends Lists, Followers and Following) - - ## Installation To install the project, clone it from GitHub and install the PHP dependencies @@ -67,21 +62,15 @@ At this point, the application is ready to run. The PHP Solid server can be run in several different ways. - - The application can be run with a Docker image of your choice or on a local environment, using Apache, NginX, or PHP's internal HTTP server. The latter is only advised in development. For security reasons, the server expects to run on HTTPS (also known as HTTP+TLS). -To run insecure, set the environment variable `ENVIRONMENT` to `develop`. This -will prohibit the application from running in production mode. - - +To run insecure, for instance when the application is run behind a proxy or in a +PHP-FPM (or similar) setup, set the environment variable `PROXY_MODE`. +This will allow the application to accept HTTP requests. ### Docker images @@ -209,71 +198,61 @@ The underlying functionality for these features is provided by: ## Development +The easiest way to develop this project is by running the environment provided +by the `docker-compose.yml` file. This can be done by running `docker-compose up`. + +This will start the application and a pubsub server in separate docker containers. + ### Project structure This project is structured as follows: - ``` . - ├── src <- Source code - ├── vendor <- Third-party and vendor code - ├── web <- Web content + ├── bin/ <- CLI scripts + ├── config/ <- Empty directory where server configuration is generated + ├── docs/ <- Documentation + ├── src/ <- Source code + ├── tests/ <- Test fixtures, Integration- and unit-tests + ├── vendor/ <- Third-party and vendor code + ├── web/ <- Web content ├── composer.json <- PHP package and dependency configuration └── README.md <- You are now here ``` - ## Contributing -Questions or feedback can be given by [opening an issue on GitHub](https://github.com/pdsinterop/php-solid-server/issues). +Questions or feedback can be given by [opening an issue on GitHub][issues-link]. All PDS Interop projects are open source and community-friendly. Any contribution is welcome! -For more details read the [contribution guidelines](CONTRIBUTING.md). +For more details read the [contribution guidelines][contributing-link]. All PDS Interop projects adhere to [the Code Manifesto](http://codemanifesto.com) -as its [code-of-conduct](CODE_OF_CONDUCT.md). Contributors are expected to abide by its terms. +as its [code-of-conduct][code-of-conduct]. Contributors are expected to abide by its terms. There is [a list of all contributors on GitHub][contributors-page]. -For a list of changes see the [CHANGELOG](CHANGELOG.md) or the GitHub releases page. +For a list of changes see the [CHANGELOG][changelog] or [the GitHub releases page][releases-page]. ## License All code created by PDS Interop is licensed under the [MIT License][license-link]. -[contributors-page]: https://github.com/pdsinterop/php-solid-server/contributors +[changelog]: CHANGELOG.md +[code-of-conduct]: CODE_OF_CONDUCT.md +[contributing-link]: CONTRIBUTING.md +[contributors-page]: https://github.com/pdsinterop/php-solid-server/contributors +[issues-link]: https://github.com/pdsinterop/php-solid-server/issues +[releases-page]: https://github.com/pdsinterop/php-solid-server/releases [keep-a-changelog-link]: https://keepachangelog.com/ [keep-a-changelog-shield]: https://img.shields.io/badge/Keep%20a%20Changelog-f15d30.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHZpZXdCb3g9IjAgMCAxODcgMTg1Ij48cGF0aCBkPSJNNjIgN2MtMTUgMy0yOCAxMC0zNyAyMmExMjIgMTIyIDAgMDAtMTggOTEgNzQgNzQgMCAwMDE2IDM4YzYgOSAxNCAxNSAyNCAxOGE4OSA4OSAwIDAwMjQgNCA0NSA0NSAwIDAwNiAwbDMtMSAxMy0xYTE1OCAxNTggMCAwMDU1LTE3IDYzIDYzIDAgMDAzNS01MiAzNCAzNCAwIDAwLTEtNWMtMy0xOC05LTMzLTE5LTQ3LTEyLTE3LTI0LTI4LTM4LTM3QTg1IDg1IDAgMDA2MiA3em0zMCA4YzIwIDQgMzggMTQgNTMgMzEgMTcgMTggMjYgMzcgMjkgNTh2MTJjLTMgMTctMTMgMzAtMjggMzhhMTU1IDE1NSAwIDAxLTUzIDE2bC0xMyAyaC0xYTUxIDUxIDAgMDEtMTItMWwtMTctMmMtMTMtNC0yMy0xMi0yOS0yNy01LTEyLTgtMjQtOC0zOWExMzMgMTMzIDAgMDE4LTUwYzUtMTMgMTEtMjYgMjYtMzMgMTQtNyAyOS05IDQ1LTV6TTQwIDQ1YTk0IDk0IDAgMDAtMTcgNTQgNzUgNzUgMCAwMDYgMzJjOCAxOSAyMiAzMSA0MiAzMiAyMSAyIDQxLTIgNjAtMTRhNjAgNjAgMCAwMDIxLTE5IDUzIDUzIDAgMDA5LTI5YzAtMTYtOC0zMy0yMy01MWE0NyA0NyAwIDAwLTUtNWMtMjMtMjAtNDUtMjYtNjctMTgtMTIgNC0yMCA5LTI2IDE4em0xMDggNzZhNTAgNTAgMCAwMS0yMSAyMmMtMTcgOS0zMiAxMy00OCAxMy0xMSAwLTIxLTMtMzAtOS01LTMtOS05LTEzLTE2YTgxIDgxIDAgMDEtNi0zMiA5NCA5NCAwIDAxOC0zNSA5MCA5MCAwIDAxNi0xMmwxLTJjNS05IDEzLTEzIDIzLTE2IDE2LTUgMzItMyA1MCA5IDEzIDggMjMgMjAgMzAgMzYgNyAxNSA3IDI5IDAgNDJ6bS00My03M2MtMTctOC0zMy02LTQ2IDUtMTAgOC0xNiAyMC0xOSAzN2E1NCA1NCAwIDAwNSAzNGM3IDE1IDIwIDIzIDM3IDIyIDIyLTEgMzgtOSA0OC0yNGE0MSA0MSAwIDAwOC0yNCA0MyA0MyAwIDAwLTEtMTJjLTYtMTgtMTYtMzEtMzItMzh6bS0yMyA5MWgtMWMtNyAwLTE0LTItMjEtN2EyNyAyNyAwIDAxLTEwLTEzIDU3IDU3IDAgMDEtNC0yMCA2MyA2MyAwIDAxNi0yNWM1LTEyIDEyLTE5IDI0LTIxIDktMyAxOC0yIDI3IDIgMTQgNiAyMyAxOCAyNyAzM3MtMiAzMS0xNiA0MGMtMTEgOC0yMSAxMS0zMiAxMXptMS0zNHYxNGgtOFY2OGg4djI4bDEwLTEwaDExbC0xNCAxNSAxNyAxOEg5NnoiLz48L3N2Zz4K [license-link]: ./LICENSE diff --git a/docker-compose.yml b/docker-compose.yml index 9057a1a..d358389 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,12 +4,21 @@ services: build: context: . environment: + # During development it can be useful to set the ENVIRONMENT to "development" + # in order to see more details about the errors. + # ENVIRONMENT: development USERNAME: alice PASSWORD: alice123 + # to run in HTTP mode, set PROXY_MODE + # PROXY_MODE: true PUBSUB_URL: http://pubsub:8080 ports: - 80:80 - 443:443 + volumes: + - .:/app/ + # @TODO: The storage directory should be mounted separately + # as it really _should_ live outside the code directory pubsub: build: context: https://github.com/pdsinterop/php-solid-pubsub-server.git#main diff --git a/src/Controller/AuthorizeController.php b/src/Controller/AuthorizeController.php index bb0a427..267d10d 100644 --- a/src/Controller/AuthorizeController.php +++ b/src/Controller/AuthorizeController.php @@ -10,62 +10,99 @@ class AuthorizeController extends ServerController final public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface { if (!isset($_SESSION['userid'])) { - $response = $this->getResponse(); - $response = $response->withStatus(302, "Approval required"); - // FIXME: Generate a proper url for this; - $baseUrl = $this->baseUrl; - $loginUrl = $baseUrl . "/login/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']); - $response = $response->withHeader("Location", $loginUrl); - return $response; + $loginUrl = $this->baseUrl . "/login/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']); + + return $this->getResponse() + ->withHeader("Location", $loginUrl) + ->withStatus(302, "Approval required") + ; } + + $queryParams = $request->getQueryParams(); + + if (! isset($queryParams['request'])) { + return $this->getResponse() + ->withStatus(400, "Bad request, missing request") + ; + } + $parser = new \Lcobucci\JWT\Parser(); + try { + $token = $parser->parse($queryParams['request']); + } catch (\Exception $exception) { + return $this->getResponse() + ->withStatus(400, $exception->getMessage()) + ; + } + try { - $token = $parser->parse($request->getQueryParams()['request']); $_SESSION["nonce"] = $token->getClaim('nonce'); - } catch(\Exception $e) { - $_SESSION["nonce"] = $request->getQueryParams()['nonce']; + } catch(\OutOfBoundsException $e) { + if (! isset($queryParams['nonce'])) { + return $this->getResponse() + ->withStatus(400, "Bad request, missing nonce") + ; + } + + $_SESSION["nonce"] = $queryParams['nonce']; } - $getVars = $request->getQueryParams(); + /*/ Prepare GET parameters for OAUTH server request /*/ + $getVars = $queryParams; + + $getVars['response_type'] = $this->getResponseType($queryParams); + $getVars['scope'] = "openid" ; + if (!isset($getVars['grant_type'])) { $getVars['grant_type'] = 'implicit'; } - $getVars['response_type'] = $this->getResponseType(); - $getVars['scope'] = "openid" ; if (!isset($getVars['redirect_uri'])) { try { $getVars['redirect_uri'] = $token->getClaim("redirect_uri"); } catch(\Exception $e) { - $response = $this->getResponse(); - $response->withStatus(400, "Bad request, missing redirect uri"); - return $response; + return $this->getResponse() + ->withStatus(400, "Bad request, missing redirect uri") + ; } } - $clientId = $getVars['client_id']; - $approval = $this->checkApproval($clientId); + + if (! isset($queryParams['client_id'])) { + return $this->getResponse() + ->withStatus(400, "Bad request, missing client_id") + ; + } + + $clientId = $getVars['client_id']; + $approval = $this->checkApproval($clientId); if (!$approval) { - $response = $this->getResponse(); - $response = $response->withStatus(302, "Approval required"); - // FIXME: Generate a proper url for this; - $baseUrl = $this->baseUrl; - $approvalUrl = $baseUrl . "/sharing/$clientId/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']); - $response = $response->withHeader("Location", $approvalUrl); - return $response; + $approvalUrl = $this->baseUrl . "/sharing/$clientId/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']); + + return $this->getResponse() + ->withHeader("Location", $approvalUrl) + ->withStatus(302, "Approval required") + ; } + // replace the request getVars with the morphed version + $request = $request->withQueryParams($getVars); + $user = new \Pdsinterop\Solid\Auth\Entity\User(); $user->setIdentifier($this->getProfilePage()); - $request = $request->withQueryParams($getVars); // replace the request getVars with the morphed version; $response = new \Laminas\Diactoros\Response(); $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response); $response = $server->respondToAuthorizationRequest($request, $user, $approval); - $response = $this->tokenGenerator->addIdTokenToResponse($response, $clientId, $this->getProfilePage(), $_SESSION['nonce'], $this->config->getPrivateKey()); - return $response; + + return $this->tokenGenerator->addIdTokenToResponse($response, + $clientId, + $this->getProfilePage(), + $_SESSION['nonce'], + $this->config->getPrivateKey() + ); } } diff --git a/src/Controller/HelloWorldController.php b/src/Controller/HelloWorldController.php index 57ddbb2..b66f50e 100644 --- a/src/Controller/HelloWorldController.php +++ b/src/Controller/HelloWorldController.php @@ -9,10 +9,67 @@ class HelloWorldController extends AbstractController { final public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface { - $response = $this->getResponse(); - $response->getBody()->write('

Hello, World!

'); + $body = <<<'HTML' +

Hello, World!

+

The following main URL are available on this server:

+ - return $response; +
+

Footnotes

+
    +
  1. Also available without trailing slash /
  2. +
  3. A file extension can be added to force a specific format. For instance /profile/card can be + requested as /profile/card.ttl to request a Turtle file, or /profile/card.json to + request a JSON-LD file +
  4. +
  5. This URL also accepts POST requests
  6. +
  7. This URL only accepts POST requests
  8. +
+
+HTML; + + return $this->createTemplateResponse($body); } } diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php index 4d997a3..8e52414 100644 --- a/src/Controller/LoginController.php +++ b/src/Controller/LoginController.php @@ -12,37 +12,46 @@ final public function __invoke(ServerRequestInterface $request, array $args): Re $postBody = $request->getParsedBody(); $response = $this->getResponse(); - // var_dump($_SESSION); - if (isset($_SESSION['userid'])) { - $user = $_SESSION['userid']; - if ($request->getQueryParams()['returnUrl']) { - $response = $response->withStatus(302, "Redirecting"); - $response = $response->withHeader("Location", $request->getQueryParams()['returnUrl']); - return $response; - } - $response->getBody()->write("

Already logged in as $user

"); - } else if ( - ($postBody['username'] == $_ENV['USERNAME'] && $postBody['password'] == $_ENV['PASSWORD']) || - ($postBody['username'] == $_SERVER['USERNAME'] && $postBody['password'] == $_SERVER['PASSWORD']) - ) { - $user = $postBody['username']; - $_SESSION['userid'] = $user; - if ($request->getQueryParams()['returnUrl']) { - $response = $response->withStatus(302, "Redirecting"); - $response = $response->withHeader("Location", $request->getQueryParams()['returnUrl']); - return $response; - } - $response->getBody()->write("

Welcome $user

\n"); - // echo("session started\n"); - //var_dump($_SESSION); + if ($request->getMethod() === 'POST') { + if (isset($_SESSION['userid'])) { + $user = $_SESSION['userid']; + + if (isset($request->getQueryParams()['returnUrl'])) { + return $response + ->withHeader("Location", $request->getQueryParams()['returnUrl']) + ->withStatus(302) + ; + } + + $response->getBody()->write("

Already logged in as $user

"); + } elseif ($postBody['username'] && $postBody['password']) { + $user = $postBody['username']; + $password = $postBody['password']; + + if ( + ($user === $_ENV['USERNAME'] && $password === $_ENV['PASSWORD']) + || ($user === $_SERVER['USERNAME'] && $password === $_SERVER['PASSWORD']) + ) { + $_SESSION['userid'] = $user; + + if (isset($request->getQueryParams()['returnUrl'])) { + return $response + ->withHeader("Location", $request->getQueryParams()['returnUrl']) + ->withStatus(302) + ; + } + + $response->getBody()->write("

Welcome $user

\n"); + } else { + $response->getBody()->write("

Login as $user failed

\n"); + } + } else { + $response->getBody()->write("

Login failed

\n"); + } } else { - // var_dump($postBody); - //echo("cookie:\n"); - //var_dump($_COOKIE); - //echo("session:\n"); - //var_dump($_SESSION); - $response->getBody()->write("

No (try posting username=alice&password=alice123)

\n"); + return $this->createTemplateResponse('login.html'); } + return $response; } } diff --git a/src/Controller/LoginPageController.php b/src/Controller/LoginPageController.php deleted file mode 100644 index 56f5a2a..0000000 --- a/src/Controller/LoginPageController.php +++ /dev/null @@ -1,14 +0,0 @@ -createTemplateResponse('login.html'); - } -} diff --git a/src/Controller/Profile/CardController.php b/src/Controller/Profile/CardController.php index 6dc95a5..45a3d82 100644 --- a/src/Controller/Profile/CardController.php +++ b/src/Controller/Profile/CardController.php @@ -27,7 +27,7 @@ class CardController extends AbstractController */ final public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface { - // @FIXME: Target file is hard-coded for not, replace with path from $request->getRequestTarget() + // @FIXME: Target file is hard-coded for now, replace with path from $request->getRequestTarget() $filePath = '/foaf.rdf'; $filesystem = $this->getFilesystem(); $extension = '.ttl'; @@ -47,11 +47,19 @@ final public function __invoke(ServerRequestInterface $request, array $args): Re $contentType = $this->getContentTypeForFormat($format); - $serverParams = $request->getServerParams(); - $url = $serverParams["REQUEST_URI"] ?? ''; + $url = (string) $request->getUri(); - /** @noinspection PhpUndefinedMethodInspection */ // Method `readRdf` is defined by plugin - $content = $filesystem->readRdf($filePath, $format, $url); + if (substr($url, -strlen($extension)) === $extension) { + $url = substr($url, 0, -strlen($extension)); + } + + try { + $content = $filesystem->readRdf($filePath, $format, $url); + } catch (\Pdsinterop\Rdf\Flysystem\Exception $exception) { + $content = $exception->getMessage(); + + return $this->createTextResponse($content)->withStatus(500); + } return $this->createTextResponse($content)->withHeader('Content-Type', $contentType); } @@ -105,6 +113,7 @@ private function getFormatForExtension(string $extension) : string $format = ''; switch ($extension) { + case '.json': case '.jsonld': $format = Format::JSON_LD; break; @@ -117,6 +126,7 @@ private function getFormatForExtension(string $extension) : string $format = Format::NOTATION_3; break; + case '.xml': case '.rdf': $format = Format::RDF_XML; break; @@ -130,5 +140,5 @@ private function getFormatForExtension(string $extension) : string } return $format; -} + } } diff --git a/src/Controller/ResourceController.php b/src/Controller/ResourceController.php index 1699029..cf20f0c 100644 --- a/src/Controller/ResourceController.php +++ b/src/Controller/ResourceController.php @@ -14,8 +14,11 @@ class ResourceController extends ServerController /** @var Server */ private $server; - private $DPop; - private $WAC; + /** @var DPop */ + private $DPop; + /** @var WAC */ + private $WAC; + //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ final public function __construct(Server $server) @@ -45,25 +48,27 @@ final public function __invoke(Request $request, array $args) : Response $allowedOrigins = $this->config->getAllowedOrigins(); $origins = $request->getHeader('Origin'); - $isAllowed = false; - foreach ($origins as $origin) { - if ($this->WAC->isAllowed($request, $webId, $origin, $allowedOrigins)) { - $isAllowed = true; - break; - } - } + if ($origins !== []) { + foreach ($origins as $origin) { + if ($this->WAC->isAllowed($request, $webId, $origin, $allowedOrigins)) { + $response = $this->server->respondToRequest($request); - if (! $isAllowed) { + return $this->WAC->addWACHeaders($request, $response, $webId); + } + } return $this->server->getResponse()->withStatus(403, 'Access denied'); - } + } else { + $response = $this->server->respondToRequest($request); - $response = $this->server->respondToRequest($request); - - return $this->WAC->addWACHeaders($request, $response, $webId); + return $this->WAC->addWACHeaders($request, $response, $webId); + } } - private function generateDefaultAcl() { - $defaultProfile = <<< EOF + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + private function generateDefaultAcl() + { + $defaultProfile = <<< EOF # Root ACL resource for the user account @prefix acl: . @prefix foaf: . @@ -74,26 +79,26 @@ private function generateDefaultAcl() { acl:accessTo ; acl:default ; acl:mode - acl:Read. + acl:Read. # The owner has full access to every resource in their pod. # Other agents have no access rights, # unless specifically authorized in other .acl resources. <#owner> - a acl:Authorization; - acl:agent <{user-profile-uri}>; - # Set the access to the root storage folder itself - acl:accessTo ; - # All resources will inherit this authorization, by default - acl:default ; - # The owner has all of the access modes allowed - acl:mode - acl:Read, acl:Write, acl:Control. + a acl:Authorization; + acl:agent <{user-profile-uri}>; + # Set the access to the root storage folder itself + acl:accessTo ; + # All resources will inherit this authorization, by default + acl:default ; + # The owner has all of the access modes allowed + acl:mode + acl:Read, acl:Write, acl:Control. EOF; - $profileUri = $this->getUserProfile(); - $defaultProfile = str_replace("{user-profile-uri}", $profileUri, $defaultProfile); - return $defaultProfile; + $profileUri = $this->getUserProfile(); + + return str_replace("{user-profile-uri}", $profileUri, $defaultProfile); } private function getUserProfile() { diff --git a/src/Controller/ServerController.php b/src/Controller/ServerController.php index afce921..6f17a9b 100644 --- a/src/Controller/ServerController.php +++ b/src/Controller/ServerController.php @@ -2,6 +2,10 @@ namespace Pdsinterop\Solid\Controller; +use Pdsinterop\Solid\Auth\Config\Client; +use Pdsinterop\Solid\Auth\Enum\Authorization; +use Pdsinterop\Solid\Auth\Factory\ConfigFactory; + abstract class ServerController extends AbstractController { protected $authServerConfig; @@ -58,20 +62,15 @@ public function createAuthServerConfig() $clientId = $_GET['client_id']; // FIXME: No request object here to get the client Id from. $client = $this->getClient($clientId); $keys = $this->getKeys(); - try { - $config = (new ConfigFactory( - $client, - $keys['encryptionKey'], - $keys['privateKey'], - $keys['publicKey'], - $this->getOpenIdEndpoints() - ))->create(); - } catch (Throwable $e) { - // var_dump($e); - } - return $config; - } + return (new ConfigFactory( + $client, + $keys['encryptionKey'], + $keys['privateKey'], + $keys['publicKey'], + $this->getOpenIdEndpoints() + ))->create(); + } public function getClient($clientId) { @@ -93,11 +92,9 @@ public function getClient($clientId) public function createConfig() { - // if (isset($_GET['client_id'])) { $clientId = $_GET['client_id']; $client = $this->getClient($clientId); - // } return (new ConfigFactory( $client, $this->keys['encryptionKey'], @@ -106,6 +103,7 @@ public function createConfig() $this->openIdConfiguration ))->create(); } + public function checkApproval($clientId) { $approval = Authorization::DENIED; @@ -121,14 +119,16 @@ public function checkApproval($clientId) return $approval; } + public function getProfilePage() : string { return $this->baseUrl . "/profile/card#me"; // FIXME: would be better to base this on the available routes if possible. } - public function getResponseType() : string + public function getResponseType($params) : string { - $responseTypes = explode(" ", $_GET['response_type'] ?? ''); + $responseTypes = explode(" ", $params['response_type'] ?? ''); + foreach ($responseTypes as $responseType) { switch ($responseType) { case "token": diff --git a/src/Controller/StorageController.php b/src/Controller/StorageController.php index d54c0a9..c4c80a9 100644 --- a/src/Controller/StorageController.php +++ b/src/Controller/StorageController.php @@ -8,7 +8,7 @@ class StorageController extends ServerController { final public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface - { + { $body = <<< EOF @prefix : <#>. @prefix inbox: <>. @@ -23,10 +23,7 @@ final public function __invoke(ServerRequestInterface $request, array $args): Re st:mtime 1576853574.389; st:size 4096. EOF; - $response = $this->getResponse(); - - $response->getBody()->write($body); - return $response - ->withHeader("Content-type", "text/turtle"); + + return $this->createTextResponse($body)->withHeader("Content-type", "text/turtle"); } } diff --git a/src/ServerConfig.php b/src/ServerConfig.php index d58794e..a2306a0 100644 --- a/src/ServerConfig.php +++ b/src/ServerConfig.php @@ -2,19 +2,24 @@ namespace Pdsinterop\Solid; -class ServerConfig { - private $path; - private $serverConfig; - private $userConfig; - - public function __construct($path) { - $this->path = $path; - $this->serverConfigFile = $this->path . "serverConfig.json"; - $this->userConfigFile = $this->path . "user.json"; - $this->serverConfig = $this->loadConfig(); - $this->userConfig = $this->loadUserConfig(); - - } +class ServerConfig +{ + ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ + private $path; + private $serverConfig; + private $userConfig; + + //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + public function __construct($path) + { + $this->path = $path; + $this->serverConfigFile = $this->path . "serverConfig.json"; + $this->userConfigFile = $this->path . "user.json"; + $this->serverConfig = $this->loadConfig(); + $this->userConfig = $this->loadUserConfig(); + + } public function getAllowedOrigins() { @@ -23,7 +28,7 @@ public function getAllowedOrigins() $serverConfig = $this->serverConfig; foreach ($serverConfig as $value) { if (isset($value['redirect_uris'])) { - foreach($value['redirect_uris'] as $url) { + foreach ($value['redirect_uris'] as $url) { $allowedOrigins[] = parse_url($url)['host']; } } @@ -32,113 +37,137 @@ public function getAllowedOrigins() return array_unique($allowedOrigins); } - private function loadConfig() { - if (!file_exists($this->serverConfigFile)) { - $keySet = $this->generateKeySet(); - $this->serverConfig = array( - "encryptionKey" => $keySet['encryptionKey'], - "privateKey" => $keySet['privateKey'] - ); - $this->saveConfig(); - } - return json_decode(file_get_contents($this->serverConfigFile), true); - } - private function saveConfig() { - file_put_contents($this->serverConfigFile, json_encode($this->serverConfig, JSON_PRETTY_PRINT)); - } - private function loadUserConfig() { - if (!file_exists($this->userConfigFile)) { - $this->userConfig = array( - "allowedClients" => array() - ); - $this->saveUserConfig(); - } - return json_decode(file_get_contents($this->userConfigFile), true); - } - private function saveUserConfig() { - file_put_contents($this->userConfigFile, json_encode($this->userConfig, JSON_PRETTY_PRINT)); - } - - /* Server data */ - public function getPrivateKey() { - return $this->serverConfig['privateKey']; - } - - public function getEncryptionKey() { - return $this->serverConfig['encryptionKey']; - } - - public function getClientConfigById($clientId) { - $clients = (array)$this->serverConfig['clients']; - - if (array_key_exists($clientId, $clients)) { - return $clients[$clientId]; - } - return null; - } - - public function saveClientConfig($clientConfig) { - $clientId = uuidv4(); - $this->serverConfig['clients'][$clientId] = $clientConfig; - $this->saveConfig(); - return $clientId; - } - - public function saveClientRegistration($origin, $clientData) { - $originHash = md5($origin); - $existingRegistration = $this->getClientRegistration($originHash); - if ($existingRegistration && isset($existingRegistration['client_name'])) { - return $originHash; - } - - $clientData['client_name'] = $origin; - $clientData['client_secret'] = md5(random_bytes(32)); - $this->serverConfig['client-' . $originHash] = $clientData; - $this->saveConfig(); - return $originHash; - } - - public function getClientRegistration($clientId) { - if (isset($this->serverConfig['client-' . $clientId])) { - return $this->serverConfig['client-' . $clientId]; - } else { - return array(); - } - } - - /* User specific data */ - public function getAllowedClients($userId) { - return $this->userConfig['allowedClients']; - } - - public function addAllowedClient($userId, $clientId) { - $this->userConfig['allowedClients'][] = $clientId; - $this->userConfig['allowedClients'] = array_unique($this->userConfig['allowedClients']); - $this->saveUserConfig(); - } - - public function removeAllowedClient($userId, $clientId) { - $this->userConfig['allowedClients'] = array_diff($this->userConfig['allowedClients'], array($clientId)); - $this->saveUserConfig(); - } - - /* Helper functions */ - private function generateKeySet() { - $config = array( - "digest_alg" => "sha256", - "private_key_bits" => 2048, - "private_key_type" => OPENSSL_KEYTYPE_RSA, - ); - // Create the private and public key - $key = openssl_pkey_new($config); - - // Extract the private key from $key to $privateKey - openssl_pkey_export($key, $privateKey); - $encryptionKey = base64_encode(random_bytes(32)); - $result = array( - "privateKey" => $privateKey, - "encryptionKey" => $encryptionKey - ); - return $result; - } + private function loadConfig() + { + if ( ! file_exists($this->serverConfigFile)) { + $keySet = $this->generateKeySet(); + $this->serverConfig = [ + "encryptionKey" => $keySet['encryptionKey'], + "privateKey" => $keySet['privateKey'], + ]; + $this->saveConfig(); + } + + return json_decode(file_get_contents($this->serverConfigFile), true); + } + + private function saveConfig() + { + file_put_contents($this->serverConfigFile, json_encode($this->serverConfig, JSON_PRETTY_PRINT)); + } + + private function loadUserConfig() + { + if ( ! file_exists($this->userConfigFile)) { + $this->userConfig = [ + "allowedClients" => [], + ]; + $this->saveUserConfig(); + } + + return json_decode(file_get_contents($this->userConfigFile), true); + } + + private function saveUserConfig() + { + file_put_contents($this->userConfigFile, json_encode($this->userConfig, JSON_PRETTY_PRINT)); + } + + /* Server data */ + public function getPrivateKey() + { + return $this->serverConfig['privateKey']; + } + + public function getEncryptionKey() + { + return $this->serverConfig['encryptionKey']; + } + + public function getClientConfigById($clientId) + { + $clients = (array) $this->serverConfig['clients']; + + if (array_key_exists($clientId, $clients)) { + return $clients[$clientId]; + } + + return null; + } + + public function saveClientConfig($clientConfig) + { + $clientId = uuidv4(); + $this->serverConfig['clients'][$clientId] = $clientConfig; + $this->saveConfig(); + + return $clientId; + } + + public function saveClientRegistration($origin, $clientData) + { + $originHash = md5($origin); + $existingRegistration = $this->getClientRegistration($originHash); + if ($existingRegistration && isset($existingRegistration['client_name'])) { + return $originHash; + } + + $clientData['client_name'] = $origin; + $clientData['client_secret'] = md5(random_bytes(32)); + $this->serverConfig['client-' . $originHash] = $clientData; + $this->saveConfig(); + + return $originHash; + } + + public function getClientRegistration($clientId) + { + if (isset($this->serverConfig['client-' . $clientId])) { + return $this->serverConfig['client-' . $clientId]; + } else { + return []; + } + } + + /* User specific data */ + public function getAllowedClients() + { + return $this->userConfig['allowedClients']; + } + + public function addAllowedClient($userId, $clientId) + { + $this->userConfig['allowedClients'][] = $clientId; + $this->userConfig['allowedClients'] = array_unique($this->userConfig['allowedClients']); + $this->saveUserConfig(); + } + + public function removeAllowedClient($userId, $clientId) + { + $this->userConfig['allowedClients'] = array_diff($this->userConfig['allowedClients'], [$clientId]); + $this->saveUserConfig(); + } + + ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\ + + private function generateKeySet() + { + $config = [ + "digest_alg" => "sha256", + "private_key_bits" => 2048, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]; + // Create the private and public key + $key = openssl_pkey_new($config); + + // Extract the private key from $key to $privateKey + openssl_pkey_export($key, $privateKey); + $encryptionKey = base64_encode(random_bytes(32)); + $result = [ + "privateKey" => $privateKey, + "encryptionKey" => $encryptionKey, + ]; + + return $result; + } } diff --git a/src/Template/card.html b/src/Template/card.html index d948414..9c8f715 100644 --- a/src/Template/card.html +++ b/src/Template/card.html @@ -1,12 +1,5 @@ - - - - - - -
-
-

- Profile in various RDF formats -

- -
+
+

Profile in various RDF formats

+
-
-
- - -
-

RDF Format

-
-
-
-
-
- - +
+ + +
+

RDF Format

+
+
+
+
+ + diff --git a/src/Template/default.html b/src/Template/default.html index 71b5204..de8a4b3 100644 --- a/src/Template/default.html +++ b/src/Template/default.html @@ -7,14 +7,13 @@ - + + - +

This will be replaced with content from other templates

diff --git a/src/Template/login.html b/src/Template/login.html index 89402d1..9a371a6 100644 --- a/src/Template/login.html +++ b/src/Template/login.html @@ -1,11 +1,12 @@ - - - -

Please login

-
- - - -
- - \ No newline at end of file + +
+
+

Please login

+ + + + +
+
+
+ diff --git a/src/Traits/HasTemplateTrait.php b/src/Traits/HasTemplateTrait.php index dce11c1..9bb2201 100644 --- a/src/Traits/HasTemplateTrait.php +++ b/src/Traits/HasTemplateTrait.php @@ -48,9 +48,9 @@ private function createTemplateFromString(string $template) : string { return <<<"TAL" -
+
{$template} -
+
TAL; } diff --git a/web/index.php b/web/index.php index 40feee9..91d0285 100644 --- a/web/index.php +++ b/web/index.php @@ -7,6 +7,7 @@ use Laminas\Diactoros\Request; use Laminas\Diactoros\Response; use Laminas\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\TextResponse; use Laminas\Diactoros\ServerRequestFactory; use Laminas\HttpHandlerRunner\Emitter\SapiEmitter; use League\Container\Container; @@ -25,7 +26,6 @@ use Pdsinterop\Solid\Controller\HttpToHttpsController; use Pdsinterop\Solid\Controller\JwksController; use Pdsinterop\Solid\Controller\LoginController; -use Pdsinterop\Solid\Controller\LoginPageController; use Pdsinterop\Solid\Controller\OpenidController; use Pdsinterop\Solid\Controller\Profile\CardController; use Pdsinterop\Solid\Controller\Profile\ProfileController; @@ -56,9 +56,9 @@ $container->add(ResponseInterface::class, Response::class); $container->share(FilesystemInterface::class, function () use ($request) { - // @FIXME: Filesystem root and the $adapter should be configurable. + // @FIXME: Filesystem $adapter should be configurable. // Implement this with `$filesystem = \MJRider\FlysystemFactory\create(getenv('STORAGE_ENDPOINT'));` - $filesystemRoot = __DIR__ . '/../tests/fixtures'; + $filesystemRoot = getenv('STORAGE_ENDPOINT') ?: __DIR__ . '/../tests/fixtures'; $adapter = new \League\Flysystem\Adapter\Local($filesystemRoot); @@ -67,7 +67,17 @@ // Create Formats objects $formats = new \Pdsinterop\Rdf\Formats(); - $serverUri = "https://" . $request->getServerParams()["SERVER_NAME"] . $request->getServerParams()["REQUEST_URI"]; // FIXME: doublecheck that this is the correct url; + $serverParams = $request->getServerParams(); + + $serverUri = ''; + if (isset($serverParams['SERVER_NAME'])) { + $serverUri = vsprintf("%s://%s%s", [ + // FIXME: doublecheck that this is the correct url; + getenv('PROXY_MODE') ? 'http' : 'https', + $serverParams['SERVER_NAME'], + $serverParams['REQUEST_URI'] ?? '', + ]); + } // Create the RDF Adapter $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf( @@ -76,11 +86,11 @@ $formats, $serverUri ); - + $filesystem = new \League\Flysystem\Filesystem($rdfAdapter); $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats)); - + $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph); $filesystem->addPlugin($plugin); @@ -89,7 +99,7 @@ $container->share(\PHPTAL::class, function () { $template = new \PHPTAL(); - $template->setTemplateRepository(__DIR__.'/../src/Template'); + $template->setTemplateRepository(__DIR__ . '/../src/Template'); return $template; }); @@ -117,7 +127,6 @@ HttpToHttpsController::class, JwksController::class, LoginController::class, - LoginPageController::class, OpenidController::class, ProfileController::class, RegisterController::class, @@ -133,12 +142,12 @@ $traitMethods = array_keys($traits); -array_walk($controllers, function ($controller) use ($container, $traits, $traitMethods) { +array_walk($controllers, static function ($controller) use ($container, $traits, $traitMethods) { $definition = $container->add($controller); $methods = get_class_methods($controller); - array_walk ($methods, function ($method) use ($definition, $traitMethods, $traits) { + array_walk ($methods, static function ($method) use ($definition, $traitMethods, $traits) { if (in_array($method, $traitMethods, true)) { $definition->addMethodCall($method, $traits[$method]); } @@ -155,18 +164,31 @@ $router->map('GET', '/{page:(?:.|/)*}', HttpToHttpsController::class)->setScheme('http'); } +/*/ To prevent "405 Method Not Allowed" from the Router we only map `/*` to + * OPTIONS when OPTIONS are actually requested. +/*/ +if ($request->getMethod() === 'OPTIONS') { + $router->map('OPTIONS', '/{path:.*}', CorsController::class); +} + $router->map('GET', '/', HelloWorldController::class); // @FIXME: CORS handling, slash-adding (and possibly others?) should be added as middleware instead of "catchall" URLs map /*/ Create URI groups /*/ +if (file_exists(__DIR__. '/favicon.ico') === false) { + $router->map('GET', '/favicon.ico', static function () { + return (new TextResponse( + '', + ))->withHeader('Content-type', 'image/svg+xml'); + }); +} + $router->map('GET', '/login', AddSlashToPathController::class); $router->map('GET', '/profile', AddSlashToPathController::class); - -$router->map('OPTIONS', '/{path:.*}', CorsController::class); $router->map('GET', '/.well-known/openid-configuration', OpenidController::class); $router->map('GET', '/jwks', JwksController::class); -$router->map('GET', '/login/', LoginPageController::class); +$router->map('GET', '/login/', LoginController::class); $router->map('POST', '/login', LoginController::class); $router->map('POST', '/login/', LoginController::class); $router->map('POST', '/register', RegisterController::class); @@ -214,7 +236,8 @@ $response = new HtmlResponse($html, $status, $exception->getHeaders()); } catch (\Throwable $exception) { - $html = "

Oh-no! The developers messed up!

{$exception->getMessage()}

"; + $class = get_class($exception); + $html = "

Oh-no! The developers messed up!

{$exception->getMessage()} ($class)

"; if (getenv('ENVIRONMENT') === 'development') { $html .=