.
19 |
20 | inbox:
21 | a ldp:BasicContainer, ldp:Container, ldp:Resource;
22 | terms:modified "2019-12-20T14:52:54Z"^^XML:dateTime;
23 | st:mtime 1576853574.389;
24 | st:size 4096.
25 | EOF;
26 |
27 | return $this->createTextResponse($body)->withHeader("Content-type", "text/turtle");
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 PDS Interop
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | php-solid-server:
4 | build:
5 | context: .
6 | container_name: php-solid-server
7 | environment:
8 | # During development it can be useful to set the ENVIRONMENT to "development"
9 | # in order to see more details about the errors.
10 | # ENVIRONMENT: development
11 | USERNAME: alice
12 | PASSWORD: alice123
13 | # To change the root from https://localhost to something sensible , set SERVER_ROOT, for example:
14 | # SERVER_ROOT: https://nextcloud.local
15 | # to run in HTTP mode, set PROXY_MODE
16 | # PROXY_MODE: true
17 | PUBSUB_URL: http://pubsub:8080
18 | ports:
19 | - 80:80
20 | - 443:443
21 | volumes:
22 | - .:/app/
23 | # @TODO: The storage directory should be mounted separately
24 | # as it really _should_ live outside the code directory
25 |
26 | pubsub:
27 | build:
28 | context: https://github.com/pdsinterop/php-solid-pubsub-server.git#main
29 | ports:
30 | - 8080:8080
31 |
--------------------------------------------------------------------------------
/site.conf:
--------------------------------------------------------------------------------
1 |
2 | DocumentRoot "/app/web/"
3 | ErrorLog ${APACHE_LOG_DIR}/error.log
4 | CustomLog ${APACHE_LOG_DIR}/access.log combined
5 |
6 | SSLEngine on
7 | SSLCertificateFile "/tls/server.cert"
8 | SSLCertificateKeyFile "/tls/server.key"
9 |
10 |
11 | SSLOptions +StdEnvVars
12 |
13 |
14 |
15 | Options Indexes FollowSymLinks
16 | AllowOverride all
17 | Require all granted
18 |
19 | DirectoryIndex index.php
20 |
21 |
22 | RewriteEngine On
23 | RewriteCond %{HTTP:Authorization} ^(.*)
24 | RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
25 |
26 | SetEnv AccessControlAllowOrigin="*"
27 | SetEnvIf Origin "^(.*)$" AccessControlAllowOrigin=$0
28 | Header add Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
29 | Header add Access-Control-Allow-Credentials true
30 | Header add Access-Control-Allow-Headers "authorization, content-type, dpop"
31 | Header add Access-Control-Allow-Methods "GET, PUT, POST, OPTIONS, DELETE, PATCH"
32 | Header add Accept-Patch: application/sparql-update
33 | Header add Access-Control-Expose-Headers: "Accept-Patch"
34 |
35 |
--------------------------------------------------------------------------------
/src/Controller/RegisterController.php:
--------------------------------------------------------------------------------
1 | withStatus(400, "Missing redirect URIs");
17 | }
18 | $clientData['client_id_issued_at'] = time();
19 | $parsedOrigin = parse_url($clientData['redirect_uris'][0]);
20 | $origin = $parsedOrigin['host'];
21 |
22 | $clientId = $this->config->saveClientRegistration($origin, $clientData);
23 |
24 | // FIXME: properly generate this url;
25 | $baseUrl = $this->baseUrl;
26 | $clientUrl = $baseUrl . "/clients/$clientId";
27 |
28 | $registration = array(
29 | 'client_id' => $clientId,
30 | 'registration_client_uri' => $clientUrl,
31 | 'client_id_issued_at' => $clientData['client_id_issued_at'],
32 | 'redirect_uris' => $clientData['redirect_uris'],
33 | );
34 |
35 | $registration = $this->tokenGenerator->respondToRegistration($registration, $this->config->getPrivateKey());
36 | return new JsonResponse($registration);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/Controller/AbstractController.php:
--------------------------------------------------------------------------------
1 | getResponse()
24 | ->withHeader('location', $url)
25 | ->withStatus($status)
26 | ;
27 | }
28 |
29 | final public function createTemplateResponse(string $template, array $context = []) : ResponseInterface
30 | {
31 | $response = $this->buildTemplate($template, $context);
32 |
33 | return $this->createTextResponse($response);
34 | }
35 |
36 | final public function createTextResponse(string $message, int $status = 200) : ResponseInterface
37 | {
38 | $response = $this->getResponse();
39 |
40 | $body = $response->getBody();
41 |
42 | $body->write($message);
43 |
44 | return $response->withBody($body)->withStatus($status);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | ---
2 | baseurl: /php-solid-server
3 | description: Standalone Solid Server written in PHP by PDS Interop
4 | repository: pdsinterop/php-solid-server
5 | title: Standalone PHP Solid Server
6 | url: https://pdsinterop.org
7 |
8 | permalink: pretty
9 | remote_theme: Potherca/extend-the-docs@v0.3.1
10 |
11 | exclude:
12 | - "bin/"
13 | - "src/"
14 | - "tests/"
15 | - "vendor/"
16 | - "Gemfile"
17 | - "*.json"
18 | - "*.lock"
19 |
20 | plugins:
21 | - github-pages
22 | - jekyll-github-metadata
23 | - jekyll-remote-theme
24 | - jekyll-seo-tag
25 |
26 | # Extend the Docs settings (see https://pother.ca/extend-the-docs/)
27 | nav:
28 | cross_repository:
29 | exclude:
30 | - jekyll-theme
31 | - pdsinterop.github.io
32 | show_archived: true
33 | show_homepage: false
34 | exclude:
35 | - /
36 | - /404.html
37 | favicon_ico : /favicon.ico
38 | main_title:
39 | link: '/'
40 | recurse: true
41 |
42 | # Just the Docs settings (see https://pmarsceill.github.io/just-the-docs/docs/configuration/)
43 | aux_links:
44 | "PDS Interop on GitHub":
45 | - https://github.com/pdsinterop
46 | footer_content: 'Copyright © 2020-2021 PDS Interop . Distributed under a MIT license .
'
47 | gh_edit_link: true
48 | gh_edit_repository: https://github.com/pdsinterop/php-solid-server/
49 | logo: https://avatars3.githubusercontent.com/u/65920341
50 | search_enabled: true
51 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.3-apache
2 |
3 | # ==============================================================================
4 | # Set up the machine
5 | # ------------------------------------------------------------------------------
6 | COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
7 |
8 | SHELL ["/bin/bash", "-o", "pipefail", "-c"]
9 |
10 | RUN export DEBIAN_FRONTEND=noninteractive \
11 | && apt-get update && \
12 | apt-get install -y --no-install-recommends \
13 | git \
14 | libzip-dev \
15 | zlib1g-dev \
16 | && rm -rf /var/lib/apt/lists/*
17 |
18 | RUN mkdir /tls && openssl req -new -x509 -days 365 -nodes \
19 | -out /tls/server.cert \
20 | -keyout /tls/server.key \
21 | -subj "/C=NL/ST=Overijssel/L=Enschede/O=PDS Interop/OU=IT/CN=pdsinterop.org"
22 |
23 | RUN docker-php-ext-install \
24 | bcmath \
25 | mbstring \
26 | mysqli \
27 | pdo \
28 | pdo_mysql \
29 | zip
30 |
31 | RUN a2enmod headers rewrite ssl
32 |
33 | COPY site.conf /etc/apache2/sites-enabled/site.conf
34 |
35 | WORKDIR /app
36 |
37 | EXPOSE 443
38 | # ==============================================================================
39 |
40 |
41 | # ==============================================================================
42 | # Add the source code
43 | # ------------------------------------------------------------------------------
44 | ARG PROJECT_PATH
45 | RUN : "${PROJECT_PATH:=$PWD}"
46 |
47 | COPY "${PROJECT_PATH}" /app/
48 |
49 | RUN composer install --no-dev --prefer-dist
50 | RUN chown -R www-data:www-data /app
51 | # ==============================================================================
52 |
--------------------------------------------------------------------------------
/src/Template/card.html:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 | Profile in various RDF formats
26 |
35 |
36 |
37 |
38 |
39 |
40 |
RDF Format
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/Controller/TokenController.php:
--------------------------------------------------------------------------------
1 | getParsedBody()['code'];
16 | $clientId = $request->getParsedBody()['client_id'];
17 | $DPop = new DPop();
18 | $dpop = $request->getServerParams()['HTTP_DPOP'];
19 | try {
20 | $dpopKey = $DPop->getDPopKey($dpop, $request);
21 | } catch(\Exception $e) {
22 | return $this->getResponse()->withStatus(409, "Invalid token");
23 | }
24 |
25 | $server = new \Pdsinterop\Solid\Auth\Server(
26 | $this->authServerFactory,
27 | $this->authServerConfig,
28 | new \Laminas\Diactoros\Response()
29 | );
30 |
31 | $response = $server->respondToAccessTokenRequest($request);
32 |
33 | // FIXME: not sure if decoding this here is the way to go.
34 | // FIXME: because this is a public page, the nonce from the session is not available here.
35 | $codeInfo = $this->tokenGenerator->getCodeInfo($code);
36 |
37 | return $this->tokenGenerator->addIdTokenToResponse($response,
38 | $clientId,
39 | $codeInfo['user_id'],
40 | $_SESSION['nonce'],
41 | $this->config->getPrivateKey(),
42 | $dpopKey
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Controller/AbstractRedirectController.php:
--------------------------------------------------------------------------------
1 | request->getRequestTarget();
23 |
24 | [$path, $query] = explode('?', $target);
25 |
26 | return $path;
27 | }
28 |
29 | /** @return string */
30 | final public function getQuery() : string
31 | {
32 | $target = $this->request->getRequestTarget();
33 |
34 | [$url, $query] = explode('?', $target);
35 |
36 | if (is_string($query)) {
37 | $query .= '?' . $query;
38 | }
39 |
40 |
41 | return (string) $query;
42 | }
43 |
44 | abstract public function getTargetUrl(): string;
45 |
46 | //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
47 |
48 | final public function __invoke(ServerRequestInterface $request, array $args): ResponseInterface
49 | {
50 | $this->request = $request;
51 | $this->args = $args;
52 |
53 | return $this->createRedirectResponse($this->getTargetUrl());
54 | }
55 |
56 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/docs/spec-compliance/server.md:
--------------------------------------------------------------------------------
1 | # Server
2 |
3 | This document describes what the Solid specification demands from a server in
4 | general. Implementation details have been added describing if and how compliance
5 | has been reached.
6 |
7 | ## Spec compliance
8 |
9 | - [x] _All http URIs MUST redirect to their https counterparts using a response with a 301 status code and a Location header._
10 | ![][ready] Implemented through the `Pdsinterop\Solid\Controller\HttpToHttpsController`.
11 |
12 | - [ ] _It SHOULD additionally implement the server part of HTTP/1.1 Caching to improve performance_
13 | ![][maybe] As caching can be added "in-front" of the server this is deemed low-priority.
14 |
15 | - [ ] _When a client does not provide valid credentials when requesting a resource that requires it, the data pod MUST send a response with a 401 status code (unless 404 is preferred for security reasons)._
16 | ![][later] This will need to be implemented as part of the OAuth, ACL, and protected documents parts.
17 |
18 | - [ ] _A Solid server MUST reject PUT, POST and PATCH requests without the Content-Type header with a status code of 400._
19 | ![][todo] This should be added in a similar fashion as the HTTPtheHTTPS mechanism. No need to continue routing if this criteria is not met.
20 |
21 | - [x] _Paths ending with a slash denote a container resource. the server MAY respond to requests for the latter URI with a 301 redirect to the former._
22 | ![][ready] Implemented through the `Pdsinterop\Solid\Controller\AddSlashToPathController`
23 |
24 | [later]: https://img.shields.io/badge/resolution-later-important.svg
25 | [maybe]: https://img.shields.io/badge/resolution-maybe%20later-yellow.svg
26 | [ready]: https://img.shields.io/badge/resolution-done-success.svg
27 | [todo]: https://img.shields.io/badge/resolution-todo-critical.svg
28 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "autoload": {
3 | "psr-4" :{
4 | "Pdsinterop\\Solid\\": "src/"
5 | }
6 | },
7 | "config": {
8 | "bin-dir": "./bin",
9 | "sort-packages": true
10 | },
11 | "description": "Standalone Solid Server written in PHP by PDS Interop.",
12 | "license": "MIT",
13 | "name": "pdsinterop/solid-server",
14 | "require": {
15 | "php": "^7.3",
16 | "ext-dom": "*",
17 | "ext-json": "*",
18 | "ext-mbstring": "*",
19 | "ext-openssl": "*",
20 | "codercat/jwk-to-pem": "^1.1",
21 | "defuse/php-encryption": "^2.3",
22 | "laminas/laminas-diactoros": " ^2.8",
23 | "laminas/laminas-httphandlerrunner": "^1.5",
24 | "lcobucci/jwt": "3.3.3",
25 | "league/container": "^3.4",
26 | "league/flysystem": "^1.1",
27 | "league/oauth2-server": "^8.1",
28 | "league/route": "^4.5",
29 | "pdsinterop/flysystem-rdf": "^0.3",
30 | "pdsinterop/solid-auth": "^0.6",
31 | "pdsinterop/solid-crud": "^0.3",
32 | "php-http/httplug": "^2.2",
33 | "phptal/phptal": "^1.5"
34 | },
35 | "require-dev": {
36 | "phpunit/phpunit": "^8.5 | ^9.5"
37 | },
38 | "scripts": {
39 | "lint":"",
40 | "serve-dev":"USERNAME=alice PASSWORD=alice123 ENVIRONMENT=development SERVER_ROOT=\"http://${HOST:-localhost}:${PORT:-8080}\" php -S \"${HOST:-localhost}:${PORT:-8080}\" -t web/ web/index.php",
41 | "serve-dev-docker":"bash ./bin/serve-docker-dev.sh",
42 | "test":"phpunit"
43 | },
44 | "scripts-descriptions": {
45 | "serve-dev": "Run the application with the internal PHP development server",
46 | "serve-dev-docker": "Run the application with the docker image provided by the TestSuite repo.",
47 | "test": "Run unit-test with PHPUnit"
48 | },
49 | "type": "project"
50 | }
51 |
--------------------------------------------------------------------------------
/src/Controller/Profile/ProfileController.php:
--------------------------------------------------------------------------------
1 | request = $request;
26 |
27 | $filesystem = $this->getFilesystem();
28 |
29 | $formats = Format::keys();
30 | $contents = $this->fetchFileContents($filesystem, $formats);
31 |
32 | return $this->createTemplateResponse('card.html', [
33 | 'files' => $contents,
34 | 'formats' => $formats,
35 | ]);
36 | }
37 |
38 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
39 |
40 | /**
41 | * @param FilesystemInterface $filesystem
42 | * @param array string[]
43 | *
44 | * @return string[]
45 | */
46 | private function fetchFileContents(FilesystemInterface $filesystem, array $formats) : array
47 | {
48 | $contents = [];
49 |
50 | $serverParams = $this->request->getServerParams();
51 | $url = $serverParams["REQUEST_URI"] ?? '';
52 |
53 | array_walk($formats, static function ($format, $index) use (&$contents, $filesystem, $url) {
54 | /** @noinspection PhpUndefinedMethodInspection */ // Method `readRdf` is defined by plugin
55 | $contents[$index] = $filesystem->readRdf('/foaf.rdf', $format, $url);
56 | });
57 |
58 | return $contents;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Controller/LoginController.php:
--------------------------------------------------------------------------------
1 | getParsedBody();
13 | $response = $this->getResponse();
14 |
15 | if ($request->getMethod() === 'POST') {
16 | if (isset($_SESSION['userid'])) {
17 | $user = $_SESSION['userid'];
18 |
19 | if (isset($request->getQueryParams()['returnUrl'])) {
20 | return $response
21 | ->withHeader("Location", $request->getQueryParams()['returnUrl'])
22 | ->withStatus(302)
23 | ;
24 | }
25 |
26 | $response->getBody()->write("Already logged in as $user ");
27 | } elseif ($postBody['username'] && $postBody['password']) {
28 | $user = $postBody['username'];
29 | $password = $postBody['password'];
30 |
31 | if (
32 | ($user === $_ENV['USERNAME'] && $password === $_ENV['PASSWORD'])
33 | || ($user === $_SERVER['USERNAME'] && $password === $_SERVER['PASSWORD'])
34 | ) {
35 | $_SESSION['userid'] = $user;
36 |
37 | if (isset($request->getQueryParams()['returnUrl'])) {
38 | return $response
39 | ->withHeader("Location", $request->getQueryParams()['returnUrl'])
40 | ->withStatus(302)
41 | ;
42 | }
43 |
44 | $response->getBody()->write("Welcome $user \n");
45 | } else {
46 | $response->getBody()->write("Login as $user failed \n");
47 | }
48 | } else {
49 | $response->getBody()->write("Login failed \n");
50 | }
51 | } else {
52 | return $this->createTemplateResponse('login.html');
53 | }
54 |
55 | return $response;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Traits/HasTemplateTrait.php:
--------------------------------------------------------------------------------
1 | template;
19 | }
20 |
21 | public function setTemplate(PHPTAL $template) : void
22 | {
23 | $this->template = $template;
24 | }
25 |
26 | public function buildTemplate(string $template, array $context) : string
27 | {
28 | $engine = $this->getTemplate();
29 |
30 | $templateExists = $this->isTemplate($template, $engine);
31 |
32 | if ($templateExists === true) {
33 | $engine->setTemplate($template);
34 | } else {
35 | $template = $this->createTemplateFromString($template);
36 |
37 | $engine->setSource($template);
38 | }
39 |
40 | $engine = $this->setContextOnTemplate($context, $engine);
41 |
42 | return $engine->execute();
43 | }
44 |
45 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
46 |
47 | private function createTemplateFromString(string $template) : string
48 | {
49 | return <<<"TAL"
50 |
51 |
52 | {$template}
53 |
54 |
55 | TAL;
56 | }
57 |
58 | private function isTemplate(string $template, \PHPTAL $engine) : bool
59 | {
60 | $templateExists = array_map(static function ($templateRepository) use ($template) {
61 | return file_exists($templateRepository . '/' . ltrim($template, '/'));
62 | }, $engine->getTemplateRepositories());
63 |
64 | return array_sum($templateExists) > 0;
65 | }
66 |
67 | private function setContextOnTemplate(array $context, \PHPTAL $engine) : \PHPTAL
68 | {
69 | array_walk($context, static function ($value, $name) use (&$engine) {
70 | $engine->set($name, $value);
71 | });
72 |
73 | return $engine;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Controller/HelloWorldController.php:
--------------------------------------------------------------------------------
1 | Hello, World!
15 | The following main URL are available on this server:
16 |
58 |
59 |
60 | Footnotes
61 |
62 | Also available without trailing slash /
63 | A file extension can be added to force a specific format. For instance /profile/card can be
64 | requested as /profile/card.ttl to request a Turtle file, or /profile/card.json to
65 | request a JSON-LD file
66 |
67 | This URL also accepts POST requests
68 | This URL only accepts POST requests
69 |
70 |
71 | HTML;
72 |
73 | return $this->createTemplateResponse($body);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Service/RouterService.php:
--------------------------------------------------------------------------------
1 | container = $container;
29 | $this->router = $router;
30 | }
31 |
32 | final public function populate() : Router
33 | {
34 | $container = $this->container;
35 | $router = $this->router;
36 |
37 | /*/ Default output is HTML, routes should return a Response object /*/
38 | $strategy = new ApplicationStrategy();
39 | $strategy->setContainer($container);
40 | $router->setStrategy($strategy);
41 |
42 | /*/ Redirect all HTTP requests to HTTPS, unless we are behind a proxy /*/
43 | if ( ! getenv('PROXY_MODE')) {
44 | $router->map('GET', '/{page:(?:.|/)*}', HttpToHttpsController::class)->setScheme('http');
45 | }
46 |
47 | /*/ Map routes and groups /*/
48 | $router->map('GET', '/', HelloWorldController::class);
49 | $router->group('/profile', $this->createProfileGroup());
50 |
51 | return $router;
52 | }
53 |
54 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
55 |
56 | private function createProfileGroup() : Callable
57 | {
58 | return static function (RouteGroup $group) {
59 | $group->map('GET', '/', AddSlashToPathController::class);
60 | $group->map('GET', '', ProfileController::class);
61 | $group->map('GET', '/card', CardController::class);
62 | $group->map('GET', '/card{extension}', CardController::class);
63 | };
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/ExceptionResponse.php:
--------------------------------------------------------------------------------
1 | exception = $exception;
25 | }
26 |
27 | final public function createResponse() : HtmlResponse
28 | {
29 | $exception = $this->exception;
30 |
31 | if ($exception instanceof HttpException) {
32 | $response = $this->respondToHttpException($exception);
33 | } else {
34 | $response = $this->responseToException($exception);
35 | }
36 |
37 | return $response;
38 | }
39 |
40 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
41 |
42 | private function isDevelop() : bool
43 | {
44 | static $isDevelop;
45 |
46 | if ($isDevelop === null) {
47 | $isDevelop = getenv('ENVIRONMENT') === 'development';
48 | }
49 |
50 | return $isDevelop;
51 | }
52 |
53 | private function responseToException(Exception $exception) : HtmlResponse
54 | {
55 | $html = "Oh-no! The developers messed up! {$exception->getMessage()}
";
56 |
57 | if ($this->isDevelop()) {
58 | $html .=
59 | "{$exception->getFile()}:{$exception->getLine()}
" .
60 | "{$exception->getTraceAsString()} ";
61 | }
62 |
63 | return new HtmlResponse($html, 500, []);
64 | }
65 |
66 | private function respondToHttpException(HttpException $exception) : HtmlResponse
67 | {
68 | $status = $exception->getStatusCode();
69 |
70 | $message = self::MESSAGE_GENERIC_ERROR;
71 |
72 | if ($exception instanceof NotFoundException) {
73 | $message = self::MESSAGE_NO_SUCH_PAGE;
74 | }
75 |
76 | $html = vsprintf('%s %s (%s)
', [
77 | $message,
78 | $exception->getMessage(),
79 | $status,
80 | ]);
81 |
82 | if ($this->isDevelop()) {
83 | $html .= "{$exception->getTraceAsString()} ";
84 | }
85 |
86 | return new HtmlResponse($html, $status, $exception->getHeaders());
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Controller/AuthorizeController.php:
--------------------------------------------------------------------------------
1 | baseUrl . "/login/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']);
15 |
16 | return $this->getResponse()
17 | ->withHeader("Location", $loginUrl)
18 | ->withStatus(302, "Approval required")
19 | ;
20 | }
21 |
22 | $queryParams = $request->getQueryParams();
23 |
24 | $parser = new \Lcobucci\JWT\Parser();
25 |
26 | try {
27 | $token = $parser->parse($request->getQueryParams()['request']);
28 | $_SESSION["nonce"] = $token->getClaim('nonce');
29 | } catch(\Exception $e) {
30 | $_SESSION["nonce"] = $request->getQueryParams()['nonce'];
31 | }
32 |
33 | /*/ Prepare GET parameters for OAUTH server request /*/
34 | $getVars = $queryParams;
35 |
36 | $getVars['response_type'] = $this->getResponseType($queryParams);
37 | $getVars['scope'] = "openid" ;
38 |
39 | if (!isset($getVars['grant_type'])) {
40 | $getVars['grant_type'] = 'implicit';
41 | }
42 |
43 | if (!isset($getVars['redirect_uri'])) {
44 | try {
45 | $getVars['redirect_uri'] = $token->getClaim("redirect_uri");
46 | } catch(\Exception $e) {
47 | return $this->getResponse()
48 | ->withStatus(400, "Bad request, missing redirect uri")
49 | ;
50 | }
51 | }
52 |
53 | if (! isset($queryParams['client_id'])) {
54 | return $this->getResponse()
55 | ->withStatus(400, "Bad request, missing client_id")
56 | ;
57 | }
58 |
59 | $clientId = $getVars['client_id'];
60 | $approval = $this->checkApproval($clientId);
61 | if (!$approval) {
62 | // FIXME: Generate a proper url for this;
63 | $approvalUrl = $this->baseUrl . "/sharing/$clientId/?returnUrl=" . urlencode($_SERVER['REQUEST_URI']);
64 |
65 | return $this->getResponse()
66 | ->withHeader("Location", $approvalUrl)
67 | ->withStatus(302, "Approval required")
68 | ;
69 | }
70 |
71 | // replace the request getVars with the morphed version
72 | $request = $request->withQueryParams($getVars);
73 |
74 | $user = new \Pdsinterop\Solid\Auth\Entity\User();
75 | $user->setIdentifier($this->getProfilePage());
76 |
77 | $response = new \Laminas\Diactoros\Response();
78 | $server = new \Pdsinterop\Solid\Auth\Server($this->authServerFactory, $this->authServerConfig, $response);
79 |
80 | $response = $server->respondToAuthorizationRequest($request, $user, $approval);
81 |
82 | return $this->tokenGenerator->addIdTokenToResponse($response,
83 | $clientId,
84 | $this->getProfilePage(),
85 | $_SESSION['nonce'],
86 | $this->config->getPrivateKey()
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/run-solid-test-suite.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | function setup {
5 | # Run the Solid test-suite
6 | docker network create testnet
7 |
8 | # Build and start Nextcloud server with code from current repo contents:
9 | docker build -t standalone-solid-server .
10 |
11 | docker build -t cookie https://github.com/pdsinterop/test-suites.git#main:servers/php-solid-server/cookie
12 | docker build -t pubsub-server https://github.com/pdsinterop/php-solid-pubsub-server.git#main
13 |
14 | docker pull solidtestsuite/webid-provider-tests:v2.0.3
15 | docker tag solidtestsuite/webid-provider-tests:v2.0.3 webid-provider-tests
16 | docker pull solidtestsuite/solid-crud-tests:v6.0.0
17 | docker tag solidtestsuite/solid-crud-tests:v6.0.0 solid-crud-tests
18 | docker pull solidtestsuite/web-access-control-tests:v7.0.0
19 | docker tag solidtestsuite/web-access-control-tests:v7.0.0 web-access-control-tests
20 | }
21 |
22 | function runPss {
23 | docker run -d --name server --network=testnet --env-file ./env-vars-for-test-image.list standalone-solid-server
24 | docker run -d --name thirdparty --network=testnet --env-file ./env-vars-for-third-party.list standalone-solid-server
25 |
26 | docker run -d --name pubsub --network=testnet pubsub-server
27 |
28 | until docker run --rm --network=testnet webid-provider-tests curl -kI https://server 2> /dev/null > /dev/null
29 | do
30 | echo Waiting for server to start, this can take up to a minute ...
31 | docker ps -a
32 | docker logs server
33 | sleep 1
34 | done
35 | docker ps -a
36 | docker logs server
37 | echo Confirmed that https://server is started now
38 |
39 | echo Getting cookie for Alice...
40 | export COOKIE="`docker run --rm --cap-add=SYS_ADMIN --network=testnet -e SERVER_TYPE=php-solid-server --env-file ./env-vars-for-test-image.list cookie`"
41 | if [[ $COOKIE == PHPSESSID* ]]
42 | then
43 | echo Successfully obtained cookie for Alice: $COOKIE
44 | else
45 | echo Error obtaining cookie for Alice, stopping.
46 | exit 1
47 | fi
48 |
49 | until docker run --rm --network=testnet webid-provider-tests curl -kI https://thirdparty 2> /dev/null > /dev/null
50 | do
51 | echo Waiting for thirdparty to start, this can take up to a minute ...
52 | docker ps -a
53 | docker logs thirdparty
54 | sleep 1
55 | done
56 | docker ps -a
57 | docker logs thirdparty
58 | echo Confirmed that https://thirdparty is started now
59 |
60 | echo Getting cookie for Bob...
61 | export COOKIE_BOB="`docker run --rm --cap-add=SYS_ADMIN --network=testnet -e SERVER_TYPE=php-solid-server --env-file ./env-vars-for-third-party.list cookie`"
62 | if [[ $COOKIE_BOB == PHPSESSID* ]]
63 | then
64 | echo Successfully obtained cookie for Bob: $COOKIE_BOB
65 | else
66 | echo Error obtaining cookie for Bob, stopping.
67 | exit 1
68 | fi
69 | }
70 |
71 | function runTests {
72 | echo "Running webid-provider tests with cookie $COOKIE"
73 | docker run --rm --network=testnet --env COOKIE="$COOKIE" --env-file ./env-vars-for-test-image.list webid-provider-tests
74 | docker run --rm --network=testnet --env COOKIE="$COOKIE" --env-file ./env-vars-for-test-image.list solid-crud-tests
75 | docker run --rm --network=testnet --env COOKIE="$COOKIE" --env COOKIE_ALICE="$COOKIE" --env COOKIE_BOB="$COOKIE_BOB" --env-file ./env-vars-for-test-image.list web-access-control-tests
76 | }
77 |
78 | function teardown {
79 | docker stop `docker ps --filter network=testnet -q`
80 | docker rm `docker ps --filter network=testnet -qa`
81 | docker network remove testnet
82 | }
83 |
84 | teardown || true
85 | setup
86 | runPss
87 | runTests
88 | # teardown
89 |
--------------------------------------------------------------------------------
/tests/fixtures/foaf.rdf:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Peachey
20 | Ben
21 |
22 |
23 |
24 | Alice
25 | 9e9c84204ba63aa49a664273c5563c0cd78cc9ea
26 |
27 |
28 |
29 |
30 |
31 | Bob
32 | 1a9daad476f0158b81bc66b7b27b438b4b4c19c0
33 |
34 |
35 |
36 | 012e360c88b2bf940e6a52de3e5bbf59ccbdada6
37 | Ben Peachey
38 | Potherca
39 |
40 |
41 | Mr.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Bobby
61 | Bob
62 | Bob Bobby
63 | Bobbers
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src/Controller/ResourceController.php:
--------------------------------------------------------------------------------
1 | server = $server;
29 | $this->DPop = new DPop();
30 | $this->WAC = new WAC($server->getFilesystem());
31 |
32 | // Make sure the root folder has an acl file, as is required by the spec;
33 | // Generate a default file granting the owner full access if there is nothing there.
34 | if (!$server->getFilesystem()->has("/storage/.acl")) {
35 | $defaultAcl = $this->generateDefaultAcl();
36 | $server->getFilesystem()->write("/storage/.acl", $defaultAcl);
37 | }
38 | }
39 |
40 | final public function __invoke(Request $request, array $args) : Response
41 | {
42 | try {
43 | $webId = $this->DPop->getWebId($request);
44 | } catch(\Exception $e) {
45 | return $this->server->getResponse()->withStatus(409, 'Invalid token');
46 | }
47 |
48 | $allowedOrigins = $this->config->getAllowedOrigins();
49 | $origins = $request->getHeader('Origin');
50 |
51 | if ($origins !== []) {
52 | foreach ($origins as $origin) {
53 | if ($this->WAC->isAllowed($request, $webId, $origin, $allowedOrigins)) {
54 | $response = $this->server->respondToRequest($request);
55 |
56 | return $this->WAC->addWACHeaders($request, $response, $webId);
57 | }
58 | }
59 | return $this->server->getResponse()->withStatus(403, 'Access denied');
60 | } else {
61 | $response = $this->server->respondToRequest($request);
62 |
63 | return $this->WAC->addWACHeaders($request, $response, $webId);
64 | }
65 | }
66 |
67 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
68 |
69 | private function generateDefaultAcl()
70 | {
71 | $defaultProfile = <<< EOF
72 | # Root ACL resource for the user account
73 | @prefix acl: .
74 | @prefix foaf: .
75 |
76 | <#public>
77 | a acl:Authorization;
78 | acl:agentClass foaf:Agent;
79 | acl:accessTo >;
80 | acl:default >;
81 | acl:mode
82 | acl:Read.
83 |
84 | # The owner has full access to every resource in their pod.
85 | # Other agents have no access rights,
86 | # unless specifically authorized in other .acl resources.
87 | <#owner>
88 | a acl:Authorization;
89 | acl:agent <{user-profile-uri}>;
90 | # Set the access to the root storage folder itself
91 | acl:accessTo >;
92 | # All resources will inherit this authorization, by default
93 | acl:default >;
94 | # The owner has all of the access modes allowed
95 | acl:mode
96 | acl:Read, acl:Write, acl:Control.
97 | EOF;
98 |
99 | $profileUri = $this->getUserProfile();
100 |
101 | return str_replace("{user-profile-uri}", $profileUri, $defaultProfile);
102 | }
103 |
104 | private function getUserProfile() {
105 | return $this->baseUrl . "/profile/card#me";
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Service/ContainerService.php:
--------------------------------------------------------------------------------
1 | container = $container;
35 | }
36 |
37 | final public function populate() : Container
38 | {
39 | $container = $this->container;
40 |
41 | /*/ Wire objects together /*/
42 | $container->delegate(new ReflectionContainer());
43 |
44 | $container->add(ServerRequestInterface::class, Request::class);
45 | $container->add(ResponseInterface::class, Response::class);
46 |
47 | $container->share(FilesystemInterface::class, $this->createFilesystem());
48 |
49 | $container->share(PHPTAL::class, $this->createTemplate());
50 |
51 | $this->addControllers($container);
52 |
53 | return $container;
54 | }
55 |
56 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
57 |
58 | private function addControllers(Container $container) : void
59 | {
60 | $controllers = [
61 | AddSlashToPathController::class,
62 | CardController::class,
63 | HelloWorldController::class,
64 | HttpToHttpsController::class,
65 | ProfileController::class,
66 | ];
67 |
68 | $traits = [
69 | 'setFilesystem' => [FilesystemInterface::class],
70 | 'setResponse' => [ResponseInterface::class],
71 | 'setTemplate' => [PHPTAL::class],
72 | ];
73 |
74 | $traitMethods = array_keys($traits);
75 |
76 | array_walk($controllers, static function ($controller) use ($container, $traits, $traitMethods) {
77 | $definition = $container->add($controller);
78 |
79 | $methods = get_class_methods($controller);
80 |
81 | array_walk($methods, static function ($method) use ($definition, $traitMethods, $traits) {
82 | if (in_array($method, $traitMethods, true)) {
83 | $definition->addMethodCall($method, $traits[$method]);
84 | }
85 | });
86 | });
87 | }
88 |
89 | private function createFilesystem() : Callable
90 | {
91 | return static function () {
92 | // @FIXME: Filesystem root and the $adapter should be configurable.
93 | // Implement this with `$filesystem = \MJRider\FlysystemFactory\create(getenv('STORAGE_ENDPOINT'));`
94 | $filesystemRoot = __DIR__ . '/../../tests/fixtures/';
95 |
96 | $adapter = new Local($filesystemRoot);
97 |
98 | $filesystem = new Filesystem($adapter);
99 | $graph = new EasyRdf_Graph();
100 | $plugin = new ReadRdf($graph);
101 | $filesystem->addPlugin($plugin);
102 |
103 | return $filesystem;
104 | };
105 | }
106 |
107 | private function createTemplate() : Callable
108 | {
109 | return static function () {
110 | $template = new PHPTAL();
111 | $template->setTemplateRepository(__DIR__ . '/../Template');
112 |
113 | return $template;
114 | };
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/Controller/Profile/CardController.php:
--------------------------------------------------------------------------------
1 | getRequestTarget()
31 | $filePath = '/foaf.rdf';
32 | $filesystem = $this->getFilesystem();
33 | $extension = '.ttl';
34 |
35 | // @TODO: Content negotiation from Accept headers
36 | //$format = $request->getHeader('Accept');
37 |
38 | if (array_key_exists('extension', $args)) {
39 | $extension = $args['extension'];
40 | }
41 |
42 | $format = $this->getFormatForExtension($extension);
43 |
44 | if (in_array($format, Format::keys()) === false) {
45 | throw new NotFoundException($request->getRequestTarget());
46 | }
47 |
48 | $contentType = $this->getContentTypeForFormat($format);
49 |
50 | $url = (string) $request->getUri();
51 |
52 | if (substr($url, -strlen($extension)) === $extension) {
53 | $url = substr($url, 0, -strlen($extension));
54 | }
55 |
56 | try {
57 | $content = $filesystem->readRdf($filePath, $format, $url);
58 | } catch (\Pdsinterop\Rdf\Flysystem\Exception $exception) {
59 | $content = $exception->getMessage();
60 |
61 | return $this->createTextResponse($content)->withStatus(500);
62 | }
63 |
64 | return $this->createTextResponse($content)->withHeader('Content-Type', $contentType);
65 | }
66 |
67 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
68 |
69 | /**
70 | * @param string $format
71 | *
72 | * @return string
73 | */
74 | private function getContentTypeForFormat(string $format) : string
75 | {
76 | $contentType = '';
77 |
78 | switch ($format) {
79 | case Format::JSON_LD:
80 | $contentType = 'application/ld+json';
81 | break;
82 |
83 | case Format::N_TRIPLES:
84 | $contentType = 'application/n-triples';
85 | break;
86 |
87 | case Format::NOTATION_3:
88 | $contentType = 'text/n3;charset=utf-8';
89 | break;
90 |
91 | case Format::RDF_XML:
92 | $contentType = 'application/rdf+xml';
93 | break;
94 |
95 | case Format::TURTLE:
96 | $contentType = 'text/turtle';
97 | break;
98 |
99 | default:
100 | break;
101 | }
102 |
103 | return $contentType;
104 | }
105 |
106 | /**
107 | * @param string $extension
108 | *
109 | * @return string
110 | */
111 | private function getFormatForExtension(string $extension) : string
112 | {
113 | $format = '';
114 |
115 | switch ($extension) {
116 | case '.json':
117 | case '.jsonld':
118 | $format = Format::JSON_LD;
119 | break;
120 |
121 | case '.nt':
122 | $format = Format::N_TRIPLES;
123 | break;
124 |
125 | case '.n3':
126 | $format = Format::NOTATION_3;
127 | break;
128 |
129 | case '.xml':
130 | case '.rdf':
131 | $format = Format::RDF_XML;
132 | break;
133 |
134 | case '.ttl':
135 | $format = Format::TURTLE;
136 | break;
137 |
138 | default:
139 | break;
140 | }
141 |
142 | return $format;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/Controller/ServerController.php:
--------------------------------------------------------------------------------
1 | config = new \Pdsinterop\Solid\ServerConfig(__DIR__.'/../../config/');
24 |
25 | $this->authServerConfig = $this->createAuthServerConfig();
26 | $this->authServerFactory = (new \Pdsinterop\Solid\Auth\Factory\AuthorizationServerFactory($this->authServerConfig))->create();
27 | $this->tokenGenerator = (new \Pdsinterop\Solid\Auth\TokenGenerator($this->authServerConfig));
28 | $this->baseUrl = isset($_ENV['SERVER_ROOT']) ? $_ENV['SERVER_ROOT'] : "https://localhost";
29 | }
30 |
31 | public function getOpenIdEndpoints() {
32 | // FIXME: would be better to base this on the available routes if possible.
33 | $this->baseUrl = isset($_ENV['SERVER_ROOT']) ? $_ENV['SERVER_ROOT'] : "https://localhost";
34 | return [
35 | 'issuer' => $this->baseUrl,
36 | 'authorization_endpoint' => $this->baseUrl . "/authorize",
37 | 'jwks_uri' => $this->baseUrl . "/jwks",
38 | "check_session_iframe" => $this->baseUrl . "/session",
39 | "end_session_endpoint" => $this->baseUrl . "/logout",
40 | "token_endpoint" => $this->baseUrl . "/token",
41 | "userinfo_endpoint" => $this->baseUrl . "/userinfo",
42 | "registration_endpoint" => $this->baseUrl . "/register",
43 | ];
44 | }
45 |
46 | public function getKeys()
47 | {
48 | $encryptionKey = $this->config->getEncryptionKey();
49 | $privateKey = $this->config->getPrivateKey();
50 | $key = openssl_pkey_get_private($privateKey);
51 | $publicKey = openssl_pkey_get_details($key)['key'];
52 |
53 | return [
54 | "encryptionKey" => $encryptionKey,
55 | "privateKey" => $privateKey,
56 | "publicKey" => $publicKey,
57 | ];
58 | }
59 |
60 | public function createAuthServerConfig()
61 | {
62 | $clientId = $_GET['client_id']; // FIXME: No request object here to get the client Id from.
63 | $client = $this->getClient($clientId);
64 | $keys = $this->getKeys();
65 |
66 | return (new ConfigFactory(
67 | $client,
68 | $keys['encryptionKey'],
69 | $keys['privateKey'],
70 | $keys['publicKey'],
71 | $this->getOpenIdEndpoints()
72 | ))->create();
73 | }
74 |
75 | public function getClient($clientId)
76 | {
77 | $clientRegistration = $this->config->getClientRegistration($clientId);
78 |
79 | if ($clientId && count($clientRegistration)) {
80 | $client = new Client(
81 | $clientId,
82 | $clientRegistration['client_secret'],
83 | $clientRegistration['redirect_uris'],
84 | $clientRegistration['client_name']
85 | );
86 | } else {
87 | $client = new Client('', '', [], '');
88 | }
89 |
90 | return $client;
91 | }
92 |
93 | public function createConfig()
94 | {
95 | $clientId = $_GET['client_id'];
96 | $client = $this->getClient($clientId);
97 |
98 | return (new ConfigFactory(
99 | $client,
100 | $this->keys['encryptionKey'],
101 | $this->keys['privateKey'],
102 | $this->keys['publicKey'],
103 | $this->openIdConfiguration
104 | ))->create();
105 | }
106 |
107 | public function checkApproval($clientId)
108 | {
109 | $approval = Authorization::DENIED;
110 |
111 | $allowedClients = $this->config->getAllowedClients($this->userId);
112 |
113 | if (
114 | $clientId === md5("tester") // FIXME: Double check that this is not a security issue; It is only here to help the test suite;
115 | || in_array($clientId, $allowedClients, true)
116 | ) {
117 | $approval = Authorization::APPROVED;
118 | }
119 |
120 | return $approval;
121 | }
122 |
123 | public function getProfilePage() : string
124 | {
125 | return $this->baseUrl . "/profile/card#me"; // FIXME: would be better to base this on the available routes if possible.
126 | }
127 |
128 | public function getResponseType($params) : string
129 | {
130 | $responseTypes = explode(" ", $params['response_type'] ?? '');
131 |
132 | foreach ($responseTypes as $responseType) {
133 | switch ($responseType) {
134 | case "token":
135 | return "token";
136 | break;
137 | case "code":
138 | return "code";
139 | break;
140 | }
141 | }
142 |
143 | return "token"; // default to token response type;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/ServerConfig.php:
--------------------------------------------------------------------------------
1 | path = $path;
17 | $this->serverConfigFile = $this->path . "serverConfig.json";
18 | $this->userConfigFile = $this->path . "user.json";
19 | $this->serverConfig = $this->loadConfig();
20 | $this->userConfig = $this->loadUserConfig();
21 |
22 | }
23 |
24 | public function getAllowedOrigins()
25 | {
26 | $allowedOrigins = [];
27 |
28 | $serverConfig = $this->serverConfig;
29 | foreach ($serverConfig as $value) {
30 | if (isset($value['redirect_uris'])) {
31 | foreach ($value['redirect_uris'] as $url) {
32 | $allowedOrigins[] = parse_url($url)['host'];
33 | }
34 | }
35 | }
36 |
37 | return array_unique($allowedOrigins);
38 | }
39 |
40 | private function loadConfig()
41 | {
42 | if ( ! file_exists($this->serverConfigFile)) {
43 | $keySet = $this->generateKeySet();
44 | $this->serverConfig = [
45 | "encryptionKey" => $keySet['encryptionKey'],
46 | "privateKey" => $keySet['privateKey'],
47 | ];
48 | $this->saveConfig();
49 | }
50 |
51 | return json_decode(file_get_contents($this->serverConfigFile), true);
52 | }
53 |
54 | private function saveConfig()
55 | {
56 | file_put_contents($this->serverConfigFile, json_encode($this->serverConfig, JSON_PRETTY_PRINT));
57 | }
58 |
59 | private function loadUserConfig()
60 | {
61 | if ( ! file_exists($this->userConfigFile)) {
62 | $this->userConfig = [
63 | "allowedClients" => [],
64 | ];
65 | $this->saveUserConfig();
66 | }
67 |
68 | return json_decode(file_get_contents($this->userConfigFile), true);
69 | }
70 |
71 | private function saveUserConfig()
72 | {
73 | file_put_contents($this->userConfigFile, json_encode($this->userConfig, JSON_PRETTY_PRINT));
74 | }
75 |
76 | /* Server data */
77 | public function getPrivateKey()
78 | {
79 | return $this->serverConfig['privateKey'];
80 | }
81 |
82 | public function getEncryptionKey()
83 | {
84 | return $this->serverConfig['encryptionKey'];
85 | }
86 |
87 | public function getClientConfigById($clientId)
88 | {
89 | $clients = (array) $this->serverConfig['clients'];
90 |
91 | if (array_key_exists($clientId, $clients)) {
92 | return $clients[$clientId];
93 | }
94 |
95 | return null;
96 | }
97 |
98 | public function saveClientConfig($clientConfig)
99 | {
100 | $clientId = uuidv4();
101 | $this->serverConfig['clients'][$clientId] = $clientConfig;
102 | $this->saveConfig();
103 |
104 | return $clientId;
105 | }
106 |
107 | public function saveClientRegistration($origin, $clientData)
108 | {
109 | $originHash = md5($origin);
110 | $existingRegistration = $this->getClientRegistration($originHash);
111 | if ($existingRegistration && isset($existingRegistration['client_name'])) {
112 | return $originHash;
113 | }
114 |
115 | $clientData['client_name'] = $origin;
116 | $clientData['client_secret'] = md5(random_bytes(32));
117 | $this->serverConfig['client-' . $originHash] = $clientData;
118 | $this->saveConfig();
119 |
120 | return $originHash;
121 | }
122 |
123 | public function getClientRegistration($clientId)
124 | {
125 | if (isset($this->serverConfig['client-' . $clientId])) {
126 | return $this->serverConfig['client-' . $clientId];
127 | } else {
128 | return [];
129 | }
130 | }
131 |
132 | /* User specific data */
133 | public function getAllowedClients()
134 | {
135 | return $this->userConfig['allowedClients'];
136 | }
137 |
138 | public function addAllowedClient($userId, $clientId)
139 | {
140 | $this->userConfig['allowedClients'][] = $clientId;
141 | $this->userConfig['allowedClients'] = array_unique($this->userConfig['allowedClients']);
142 | $this->saveUserConfig();
143 | }
144 |
145 | public function removeAllowedClient($userId, $clientId)
146 | {
147 | $this->userConfig['allowedClients'] = array_diff($this->userConfig['allowedClients'], [$clientId]);
148 | $this->saveUserConfig();
149 | }
150 |
151 | ////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
152 |
153 | private function generateKeySet()
154 | {
155 | $config = [
156 | "digest_alg" => "sha256",
157 | "private_key_bits" => 2048,
158 | "private_key_type" => OPENSSL_KEYTYPE_RSA,
159 | ];
160 | // Create the private and public key
161 | $key = openssl_pkey_new($config);
162 |
163 | // Extract the private key from $key to $privateKey
164 | openssl_pkey_export($key, $privateKey);
165 | $encryptionKey = base64_encode(random_bytes(32));
166 | $result = [
167 | "privateKey" => $privateKey,
168 | "encryptionKey" => $encryptionKey,
169 | ];
170 |
171 | return $result;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [v0.6.2](https://github.com/pdsinterop/php-solid-server/tree/v0.6.2) (2022-01-13)
4 |
5 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.6.1...v0.6.2)
6 |
7 | ## [v0.6.1](https://github.com/pdsinterop/php-solid-server/tree/v0.6.1) (2022-01-12)
8 |
9 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.6.0...v0.6.1)
10 |
11 | ### Pull request(s) without label
12 |
13 | - Code cleanup [\#59](https://github.com/pdsinterop/php-solid-server/pull/59) (@Potherca)
14 |
15 | ## [v0.6.0](https://github.com/pdsinterop/php-solid-server/tree/v0.6.0) (2022-01-11)
16 |
17 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.5.2...v0.6.0)
18 |
19 | ### Closes
20 |
21 | - Cannot assign requested address [\#26](https://github.com/pdsinterop/php-solid-server/issues/26)
22 | - New release \(v0.6.0\) [\#51](https://github.com/pdsinterop/php-solid-server/issues/51)
23 | - Which features are a available and which are not? [\#46](https://github.com/pdsinterop/php-solid-server/issues/46)
24 | - when ENVIRONMENT=development is set, the server does not work on https [\#16](https://github.com/pdsinterop/php-solid-server/issues/16)
25 |
26 | ### Pull request(s) without label
27 |
28 | - Cleanup for v0.6 release. [\#58](https://github.com/pdsinterop/php-solid-server/pull/58) (@Potherca)
29 | - Run latest versions of test suites [\#53](https://github.com/pdsinterop/php-solid-server/pull/53) (@michielbdejong)
30 |
31 | ## [v0.5.2](https://github.com/pdsinterop/php-solid-server/tree/v0.5.2) (2021-11-26)
32 |
33 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.5.1...v0.5.2)
34 |
35 | ### Closes
36 |
37 | - Instructions for running with docker-compose [\#40](https://github.com/pdsinterop/php-solid-server/issues/40)
38 | - Tests failing in master [\#34](https://github.com/pdsinterop/php-solid-server/issues/34)
39 | - New webid-provider test failing in master [\#31](https://github.com/pdsinterop/php-solid-server/issues/31)
40 |
41 | ### Pull request(s) without label
42 |
43 | - Latest test suites [\#50](https://github.com/pdsinterop/php-solid-server/pull/50) (@michielbdejong)
44 | - Pin test suite versions [\#49](https://github.com/pdsinterop/php-solid-server/pull/49) (@michielbdejong)
45 | - Switch from Travis to GitHub Actions [\#47](https://github.com/pdsinterop/php-solid-server/pull/47) (@michielbdejong)
46 | - adding origin check with WAC [\#44](https://github.com/pdsinterop/php-solid-server/pull/44) (@ylebre)
47 | - Fix some outdated documentation and scripts [\#41](https://github.com/pdsinterop/php-solid-server/pull/41) (@NoelDeMartin)
48 | - Merge pull request \#33 from pdsinterop/add-wac-tests [\#39](https://github.com/pdsinterop/php-solid-server/pull/39) (@ylebre)
49 | - Add wac tests [\#38](https://github.com/pdsinterop/php-solid-server/pull/38) (@poef)
50 | - WIP: dpop/webid [\#37](https://github.com/pdsinterop/php-solid-server/pull/37) (@ylebre)
51 | - Updating branch before starting [\#36](https://github.com/pdsinterop/php-solid-server/pull/36) (@ylebre)
52 | - Fix solid tests [\#35](https://github.com/pdsinterop/php-solid-server/pull/35) (@michielbdejong)
53 | - Add WAC tests [\#33](https://github.com/pdsinterop/php-solid-server/pull/33) (@michielbdejong)
54 | - Use the latest version of the webid-provider-tests [\#32](https://github.com/pdsinterop/php-solid-server/pull/32) (@michielbdejong)
55 | - Pull from Docker hub [\#30](https://github.com/pdsinterop/php-solid-server/pull/30) (@michielbdejong)
56 | - Add solid-crud tests [\#20](https://github.com/pdsinterop/php-solid-server/pull/20) (@michielbdejong)
57 |
58 | ## [v0.5.1](https://github.com/pdsinterop/php-solid-server/tree/v0.5.1) (2020-11-12)
59 |
60 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.5.0...v0.5.1)
61 |
62 | ### Pull request(s) without label
63 |
64 | - Fix contributing link in README [\#29](https://github.com/pdsinterop/php-solid-server/pull/29) (@NoelDeMartin)
65 |
66 | ## [v0.5.0](https://github.com/pdsinterop/php-solid-server/tree/v0.5.0) (2020-11-09)
67 |
68 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.4.0...v0.5.0)
69 |
70 | ### Closes
71 |
72 | - openid-configuration announces "localhost" instead of "server" [\#25](https://github.com/pdsinterop/php-solid-server/issues/25)
73 | - /.well-known/openid-configuration broken in master? [\#24](https://github.com/pdsinterop/php-solid-server/issues/24)
74 |
75 | ### Pull request(s) without label
76 |
77 | - Update test runner [\#27](https://github.com/pdsinterop/php-solid-server/pull/27) (@michielbdejong)
78 |
79 | ## [v0.4.0](https://github.com/pdsinterop/php-solid-server/tree/v0.4.0) (2020-10-23)
80 |
81 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.3.0...v0.4.0)
82 |
83 | ### Closes
84 |
85 | - config file not found? [\#19](https://github.com/pdsinterop/php-solid-server/issues/19)
86 | - Trailing slash in issuer [\#22](https://github.com/pdsinterop/php-solid-server/issues/22)
87 |
88 | ### Pull request(s) without label
89 |
90 | - Improve tests [\#21](https://github.com/pdsinterop/php-solid-server/pull/21) (@michielbdejong)
91 | - added token response with id token [\#17](https://github.com/pdsinterop/php-solid-server/pull/17) (@ylebre)
92 | - Run test suite on Travis CI [\#15](https://github.com/pdsinterop/php-solid-server/pull/15) (@michielbdejong)
93 | - Change flysystem-rdf dependency to dev-master as dev-dev does not exist. [\#23](https://github.com/pdsinterop/php-solid-server/pull/23) (@Potherca)
94 | - Add resources api calls [\#18](https://github.com/pdsinterop/php-solid-server/pull/18) (@Potherca)
95 |
96 | ## [v0.3.0](https://github.com/pdsinterop/php-solid-server/tree/v0.3.0) (2020-09-28)
97 |
98 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.2.0...v0.3.0)
99 |
100 | ### Closes
101 |
102 | - `HOST=127.0.0.1` vs `HOST=localhost` [\#8](https://github.com/pdsinterop/php-solid-server/issues/8)
103 | - possible helpful codebase [\#3](https://github.com/pdsinterop/php-solid-server/issues/3)
104 | - Add /.well-known/openid-configuration endpoint [\#6](https://github.com/pdsinterop/php-solid-server/issues/6)
105 |
106 | ### Pull request(s) without label
107 |
108 | - WIP: achieve login with solid.community [\#14](https://github.com/pdsinterop/php-solid-server/pull/14) (@ylebre)
109 | - merge dev branch [\#13](https://github.com/pdsinterop/php-solid-server/pull/13) (@michielbdejong)
110 | - Authorize end point [\#11](https://github.com/pdsinterop/php-solid-server/pull/11) (@michielbdejong)
111 |
112 | ## [v0.2.0](https://github.com/pdsinterop/php-solid-server/tree/v0.2.0) (2020-09-12)
113 |
114 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.1.1...v0.2.0)
115 |
116 | ### Pull request(s) without label
117 |
118 | - Add templating [\#5](https://github.com/pdsinterop/php-solid-server/pull/5) (@Potherca)
119 | - Second draft: Hard-coded profile card [\#2](https://github.com/pdsinterop/php-solid-server/pull/2) (@Potherca)
120 |
121 | ## [v0.1.1](https://github.com/pdsinterop/php-solid-server/tree/v0.1.1) (2020-07-01)
122 |
123 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.1.0...v0.1.1)
124 |
125 | ## [v0.1.0](https://github.com/pdsinterop/php-solid-server/tree/v0.1.0) (2020-06-30)
126 |
127 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/v0.0.0...v0.1.0)
128 |
129 | ### Pull request(s) without label
130 |
131 | - First draft [\#1](https://github.com/pdsinterop/php-solid-server/pull/1) (@Potherca)
132 |
133 | ## [v0.0.0](https://github.com/pdsinterop/php-solid-server/tree/v0.0.0) (2020-06-12)
134 |
135 | [Full Changelog](https://github.com/pdsinterop/php-solid-server/compare/8e7c63f389ea45da60141cd1a4f59ff467046268...v0.0.0)
136 |
137 |
138 |
139 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
140 |
--------------------------------------------------------------------------------
/.github_changelog_generator:
--------------------------------------------------------------------------------
1 | # ==============================================================================
2 | # Project Specific
3 | # ------------------------------------------------------------------------------
4 | user=pdsinterop
5 | project=php-solid-server
6 | # ------------------------------------------------------------------------------
7 | breaking-labels=backwards-incompatible, Backwards incompatible, breaking
8 | bug-labels=bug
9 | deprecated-labels=deprecated, Deprecated, Type: Deprecated
10 | enhancement-labels=improvement, documentation, enhancement
11 | exclude-labels=question, duplicate,invalid
12 | removed-labels=removed, Removed, Type: Removed
13 | security-labels=security, Security, Type: Security
14 | summary-labels=Release summary, release-summary, Summary, summary
15 | unreleased-label=Unreleased
16 | # ==============================================================================
17 |
18 | # ==============================================================================
19 | # Organisation-wide
20 | # ------------------------------------------------------------------------------
21 | # Output and content related
22 | # ------------------------------------------------------------------------------
23 | date-format=%Y-%m-%d
24 | output=CHANGELOG.md
25 | header=# Changelog
26 | breaking-prefix=### Breaking changes
27 | bug-prefix=### Fixes
28 | deprecated-prefix=### Deprecates
29 | enhancement-prefix=### Changes
30 | issue-prefix=### Closes
31 | merge-prefix=### Pull request(s) without label
32 | removed-prefix=### Removes
33 | security-prefix=### Security
34 | # ------------------------------------------------------------------------------
35 | add-issues-wo-labels=true
36 | add-pr-wo-labels=true
37 | author=true
38 | compare-link=true
39 | filter-issues-by-milestone=true
40 | # http-cache=true
41 | issues=true
42 | pulls=true
43 | # unreleased-only=true
44 | unreleased=true
45 | usernames-as-github-logins=true
46 | verbose=false
47 | # ==============================================================================
48 |
49 | ;user USER Username of the owner of the target GitHub repo OR the namespace of target Github repo if owned by an organization.
50 | ;project PROJECT Name of project on GitHub.
51 | ;token TOKEN To make more than 50 requests per hour your GitHub token is required. You can generate it at: https://github.com/settings/tokens/new
52 | ;date-format FORMAT Date format. Default is %Y-%m-%d.
53 | ;output NAME Output file. To print to STDOUT instead, use blank as path. Default is CHANGELOG.md
54 | ;base NAME Optional base file to append generated changes to. Default is HISTORY.md
55 | ;summary-label LABEL Set up custom label for the release summary section. Default is "".
56 | ;breaking-label LABEL Set up custom label for the breaking changes section. Default is "**Breaking changes:**".
57 | ;enhancement-label LABEL Set up custom label for enhancements section. Default is "**Implemented enhancements:**".
58 | ;bugs-label LABEL Set up custom label for bug-fixes section. Default is "**Fixed bugs:**".
59 | ;deprecated-label LABEL Set up custom label for the deprecated changes section. Default is "**Deprecated:**".
60 | ;removed-label LABEL Set up custom label for the removed changes section. Default is "**Removed:**".
61 | ;security-label LABEL Set up custom label for the security changes section. Default is "**Security fixes:**".
62 | ;issues-label LABEL Set up custom label for closed-issues section. Default is "**Closed issues:**".
63 | ;header-label LABEL Set up custom header label. Default is "# Changelog".
64 | ;configure-sections STRING Define your own set of sections which overrides all default sections.
65 | ;add-sections HASH, STRING Add new sections but keep the default sections.
66 | ;front-matter JSON Add YAML front matter. Formatted as JSON because it's easier to add on the command line.
67 | ;pr-label LABEL Set up custom label for pull requests section. Default is "**Merged pull requests:**".
68 | ;issues Include closed issues in changelog. Default is true.
69 | ;issues-wo-labels Include closed issues without labels in changelog. Default is true.
70 | ;pr-wo-labels Include pull requests without labels in changelog. Default is true.
71 | ;pull-requests Include pull-requests in changelog. Default is true.
72 | ;filter-by-milestone Use milestone to detect when issue was resolved. Default is true.
73 | ;issues-of-open-milestones Include issues of open milestones. Default is true.
74 | ;author Add author of pull request at the end. Default is true.
75 | ;usernames-as-github-logins Use GitHub tags instead of Markdown links for the author of an issue or pull-request.
76 | ;unreleased-only Generate log from unreleased closed issues only.
77 | ;unreleased Add to log unreleased closed issues. Default is true.
78 | ;unreleased-label LABEL Set up custom label for unreleased closed issues section. Default is "**Unreleased:**".
79 | ;compare-link Include compare link (Full Changelog) between older version and newer version. Default is true.
80 | ;include-labels x,y,z Of the labeled issues, only include the ones with the specified labels.
81 | ;exclude-labels x,y,z Issues with the specified labels will be excluded from changelog. Default is 'duplicate,question,invalid,wontfix'.
82 | ;summary-labels x,y,z Issues with these labels will be added to a new section, called "Release Summary". The section display only body of issues. Default is 'release-summary,summary'.
83 | ;breaking-labels x,y,z Issues with these labels will be added to a new section, called "Breaking changes". Default is 'backwards-incompatible,breaking'.
84 | ;enhancement-labels x,y,z Issues with the specified labels will be added to "Implemented enhancements" section. Default is 'enhancement,Enhancement'.
85 | ;bug-labels x,y,z Issues with the specified labels will be added to "Fixed bugs" section. Default is 'bug,Bug'.
86 | ;deprecated-labels x,y,z Issues with the specified labels will be added to a section called "Deprecated". Default is 'deprecated,Deprecated'.
87 | ;removed-labels x,y,z Issues with the specified labels will be added to a section called "Removed". Default is 'removed,Removed'.
88 | ;security-labels x,y,z Issues with the specified labels will be added to a section called "Security fixes". Default is 'security,Security'.
89 | ;issue-line-labels x,y,z The specified labels will be shown in brackets next to each matching issue. Use "ALL" to show all labels. Default is [].
90 | ;include-tags-regex REGEX Apply a regular expression on tag names so that they can be included, for example: --include-tags-regex ".*+d{1,}".
91 | ;exclude-tags x,y,z Changelog will exclude specified tags
92 | ;exclude-tags-regex REGEX Apply a regular expression on tag names so that they can be excluded, for example: --exclude-tags-regex ".*+d{1,}".
93 | ;since-tag x Changelog will start after specified tag.
94 | ;due-tag x Changelog will end before specified tag.
95 | ;since-commit x Fetch only commits after this time. eg. "2017-01-01 10:00:00"
96 | ;max-issues NUMBER Maximum number of issues to fetch from GitHub. Default is unlimited.
97 | ;release-url URL The URL to point to for release links, in printf format (with the tag as variable).
98 | ;github-site URL The Enterprise GitHub site where your project is hosted.
99 | ;github-api URL The enterprise endpoint to use for your GitHub API.
100 | ;simple-list Create a simple list from issues and pull requests. Default is false.
101 | ;future-release VERSION Put the unreleased changes in the specified release number.
102 | ;release-branch BRANCH Limit pull requests to the release branch, such as master or release.
103 | ;http-cache Use HTTP Cache to cache GitHub API requests (useful for large repos). Default is true.
104 | ;cache-file CACHE-FILE Filename to use for cache. Default is github-changelog-http-cache in a temporary directory.
105 | ;cache-log CACHE-LOG Filename to use for cache log. Default is github-changelog-logger.log in a temporary directory.
106 | ;config-file CONFIG-FILE Path to configuration file. Default is .github_changelog_generator.
107 | ;ssl-ca-file PATH Path to cacert.pem file. Default is a bundled lib/github_changelog_generator/ssl_certs/cacert.pem. Respects SSL_CA_PATH.
108 | ;require x,y,z Path to Ruby file(s) to require before generating changelog.
109 | ;verbose Run verbosely. Default is true.
110 |
--------------------------------------------------------------------------------
/web/index.php:
--------------------------------------------------------------------------------
1 | delegate(new ReflectionContainer());
54 |
55 | $container->add(ServerRequestInterface::class, Request::class);
56 | $container->add(ResponseInterface::class, Response::class);
57 |
58 | $container->share(FilesystemInterface::class, function () use ($request) {
59 | // @FIXME: Filesystem $adapter should be configurable.
60 | // Implement this with `$filesystem = \MJRider\FlysystemFactory\create(getenv('STORAGE_ENDPOINT'));`
61 | $filesystemRoot = getenv('STORAGE_ENDPOINT') ?: __DIR__ . '/../tests/fixtures';
62 |
63 | $adapter = new \League\Flysystem\Adapter\Local($filesystemRoot);
64 |
65 | $graph = new \EasyRdf_Graph();
66 |
67 | // Create Formats objects
68 | $formats = new \Pdsinterop\Rdf\Formats();
69 |
70 | $serverParams = $request->getServerParams();
71 |
72 | $serverUri = '';
73 | if (isset($serverParams['SERVER_NAME'])) {
74 | $serverUri = vsprintf("%s://%s%s", [
75 | // FIXME: doublecheck that this is the correct url;
76 | getenv('PROXY_MODE') ? 'http' : 'https',
77 | $serverParams['SERVER_NAME'],
78 | $serverParams['REQUEST_URI'] ?? '',
79 | ]);
80 | }
81 |
82 | // Create the RDF Adapter
83 | $rdfAdapter = new \Pdsinterop\Rdf\Flysystem\Adapter\Rdf(
84 | $adapter,
85 | $graph,
86 | $formats,
87 | $serverUri
88 | );
89 |
90 | $filesystem = new \League\Flysystem\Filesystem($rdfAdapter);
91 |
92 | $filesystem->addPlugin(new \Pdsinterop\Rdf\Flysystem\Plugin\AsMime($formats));
93 |
94 | $plugin = new \Pdsinterop\Rdf\Flysystem\Plugin\ReadRdf($graph);
95 | $filesystem->addPlugin($plugin);
96 |
97 | return $filesystem;
98 | });
99 |
100 | $container->share(\PHPTAL::class, function () {
101 | $template = new \PHPTAL();
102 | $template->setTemplateRepository(__DIR__ . '/../src/Template');
103 | return $template;
104 | });
105 |
106 | $container->add(ResourceController::class, function () use ($container) {
107 | $filesystem = $container->get(FilesystemInterface::class);
108 |
109 | $server = new ResourceServer($filesystem, new Response());
110 |
111 | $baseUrl = getenv('SERVER_ROOT') ?: "https://" . $_SERVER["SERVER_NAME"];
112 | $pubsub = getenv('PUBSUB_URL') ?: "http://" .$_SERVER["SERVER_NAME"] . ":8080/";
113 | $server->setBaseUrl($baseUrl);
114 | $server->setPubSubUrl($pubsub);
115 |
116 | return new ResourceController($server);
117 | });
118 |
119 | $controllers = [
120 | AddSlashToPathController::class,
121 | ApprovalController::class,
122 | AuthorizeController::class,
123 | CardController::class,
124 | CorsController::class,
125 | HandleApprovalController::class,
126 | HelloWorldController::class,
127 | HttpToHttpsController::class,
128 | JwksController::class,
129 | LoginController::class,
130 | OpenidController::class,
131 | ProfileController::class,
132 | RegisterController::class,
133 | StorageController::class,
134 | TokenController::class,
135 | ];
136 |
137 | $traits = [
138 | 'setFilesystem' => [FilesystemInterface::class],
139 | 'setResponse' => [ResponseInterface::class],
140 | 'setTemplate' => [\PHPTAL::class],
141 | ];
142 |
143 | $traitMethods = array_keys($traits);
144 |
145 | array_walk($controllers, static function ($controller) use ($container, $traits, $traitMethods) {
146 | $definition = $container->add($controller);
147 |
148 | $methods = get_class_methods($controller);
149 |
150 | array_walk ($methods, static function ($method) use ($definition, $traitMethods, $traits) {
151 | if (in_array($method, $traitMethods, true)) {
152 | $definition->addMethodCall($method, $traits[$method]);
153 | }
154 | });
155 | });
156 |
157 | $strategy->setContainer($container);
158 |
159 | /*/ Default output is HTML, should return a Response object /*/
160 | $router->setStrategy($strategy);
161 |
162 | /*/ Redirect all HTTP requests to HTTPS, unless we are behind a proxy /*/
163 | if ( ! getenv('PROXY_MODE')) {
164 | $router->map('GET', '/{page:(?:.|/)*}', HttpToHttpsController::class)->setScheme('http');
165 | }
166 |
167 | /*/ To prevent "405 Method Not Allowed" from the Router we only map `/*` to
168 | * OPTIONS when OPTIONS are actually requested.
169 | /*/
170 | if ($request->getMethod() === 'OPTIONS') {
171 | $router->map('OPTIONS', '/{path:.*}', CorsController::class);
172 | }
173 |
174 | $router->map('GET', '/', HelloWorldController::class);
175 |
176 | // @FIXME: CORS handling, slash-adding (and possibly others?) should be added as middleware instead of "catchall" URLs map
177 |
178 | /*/ Create URI groups /*/
179 | if (file_exists(__DIR__. '/favicon.ico') === false) {
180 | $router->map('GET', '/favicon.ico', static function () {
181 | return (new TextResponse(
182 | ' ',
183 | ))->withHeader('Content-type', 'image/svg+xml');
184 | });
185 | }
186 |
187 | $router->map('GET', '/login', AddSlashToPathController::class);
188 | $router->map('GET', '/profile', AddSlashToPathController::class);
189 | $router->map('GET', '/.well-known/openid-configuration', OpenidController::class);
190 | $router->map('GET', '/jwks', JwksController::class);
191 | $router->map('GET', '/login/', LoginController::class);
192 | $router->map('POST', '/login', LoginController::class);
193 | $router->map('POST', '/login/', LoginController::class);
194 | $router->map('POST', '/register', RegisterController::class);
195 | $router->map('GET', '/profile/', ProfileController::class);
196 | $router->map('GET', '/profile/card', CardController::class);
197 | $router->map('GET', '/profile/card{extension}', CardController::class);
198 | $router->map('GET', '/authorize', AuthorizeController::class);
199 | $router->map('GET', '/sharing/{clientId}/', ApprovalController::class);
200 | $router->map('POST', '/sharing/{clientId}/', HandleApprovalController::class);
201 | $router->map('POST', '/token', TokenController::class);
202 | $router->map('POST', '/token/', TokenController::class);
203 |
204 | $router->group('/storage', static function (\League\Route\RouteGroup $group) {
205 | $methods = [
206 | 'DELETE',
207 | 'GET',
208 | 'HEAD',
209 | // 'OPTIONS', // @TODO: This breaks because of the CorsController being added to `OPTION /*` in the index.php
210 | 'PATCH',
211 | 'POST',
212 | 'PUT',
213 | ];
214 |
215 | array_walk($methods, static function ($method) use (&$group) {
216 | $group->map($method, '/', AddSlashToPathController::class);
217 | $group->map($method, '{path:.*}', ResourceController::class);
218 | });
219 | });
220 |
221 | try {
222 | $response = $router->dispatch($request);
223 | } catch (HttpException $exception) {
224 | $status = $exception->getStatusCode();
225 |
226 | $message = 'Yeah, that\'s an error.';
227 | if ($exception instanceof NotFoundException) {
228 | $message = 'No such page.';
229 | }
230 |
231 | $html = "{$message} {$exception->getMessage()} ({$status})
";
232 |
233 | if (getenv('ENVIRONMENT') === 'development') {
234 | $html .= "{$exception->getTraceAsString()} ";
235 | }
236 |
237 | $response = new HtmlResponse($html, $status, $exception->getHeaders());
238 | } catch (\Throwable $exception) {
239 | $class = get_class($exception);
240 | $html = "Oh-no! The developers messed up! {$exception->getMessage()} ($class)
";
241 |
242 | if (getenv('ENVIRONMENT') === 'development') {
243 | $html .=
244 | "{$exception->getFile()}:{$exception->getLine()}
" .
245 | "{$exception->getTraceAsString()} "
246 | ;
247 | }
248 |
249 | $response = new HtmlResponse($html, 500, []);
250 | }
251 |
252 | // send the response to the browser
253 | $emitter->emit($response);
254 | exit;
255 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Standalone PHP Solid Server
2 |
3 | [![Project stage: Development][project-stage-badge: Development]][project-stage-page]
4 | [![License][license-shield]][license-link]
5 | [![Latest Version][version-shield]][version-link]
6 | ![Maintained][maintained-shield]
7 |
8 | [![PDS Interop][pdsinterop-shield]][pdsinterop-site]
9 | [![standard-readme compliant][standard-readme-shield]][standard-readme-link]
10 | [![keep-a-changelog compliant][keep-a-changelog-shield]][keep-a-changelog-link]
11 |
12 | _Standalone Solid Server written in PHP by PDS Interop_
13 |
14 | ## Table of Contents
15 |
16 |
17 |
18 | - [Background](#background)
19 | - [Installation](#installation)
20 | - [Usage](#usage)
21 | - [Docker images](#docker-images)
22 | - [Local environment](#local-environment)
23 | - [Built-in PHP HTTP server](#built-in-php-http-server)
24 | - [Security](#security)
25 | - [Running solid/webid-provider-tests](#running-solidwebid-provider-tests)
26 | - [Available Features](#available-features)
27 | - [Development](#development)
28 | - [Project structure](#project-structure)
29 | - [Testing](#testing)
30 | - [Contributing](#contributing)
31 | - [License](#license)
32 |
33 |
34 |
35 | ## Background
36 |
37 | The Solid specifications defines what makes a "Solid Server". Parts of
38 | those specifications are still likely to change, but at the time of this writing,
39 | they define:
40 |
41 | - Authentication
42 | - Authorization (and access control)
43 | - Content representation
44 | - Identity
45 | - Profiles
46 | - Resource (reading and writing) API
47 | - Social Web App Protocols (Notifications, Friends Lists, Followers and Following)
48 |
49 | ## Installation
50 |
51 | To install the project, clone it from GitHub and install the PHP dependencies
52 | using Composer:
53 |
54 | ```sh
55 | git clone git://github.com/pdsinterop/php-solid-server.git \
56 | && cd php-solid-server \
57 | && composer install --no-dev --prefer-dist
58 | ```
59 | At this point, the application is ready to run.
60 |
61 | ## Usage
62 |
63 | The PHP Solid server can be run in several different ways.
64 |
65 | The application can be run with a Docker image of your choice or on a local
66 | environment, using Apache, NginX, or PHP's internal HTTP server. The latter is
67 | only advised in development.
68 |
69 | For security reasons, the server expects to run on HTTPS (also known as HTTP+TLS).
70 |
71 | To run insecure, for instance when the application is run behind a proxy or in a
72 | PHP-FPM (or similar) setup, set the environment variable `PROXY_MODE`.
73 | This will allow the application to accept HTTP requests.
74 |
75 | ### Docker images
76 |
77 | When running with your own Docker image, make sure to mount the project folder
78 | to wherever the image expects it to be, e.g. `/app` or `/var/www`.
79 |
80 | For instance:
81 |
82 | ```
83 | export PORT=8080 && \
84 | docker run \
85 | --env "PORT=${PORT}" \
86 | --expose "${PORT}" \
87 | --network host \
88 | --rm \
89 | --volume "$PWD:/app" \
90 | -it \
91 | php:7.3 \
92 | php --docroot /app/web/ --server "localhost:${PORT}" /app/web/index.php
93 | ```
94 | Or on Mac:
95 | ```
96 | export PORT=8080 && \
97 | docker run \
98 | --env "PORT=${PORT}" \
99 | --expose "${PORT}" \
100 | -p "${PORT}:${PORT}" \
101 | --rm \
102 | --volume "$PWD:/app" \
103 | -it \
104 | php:7.3 \
105 | php --docroot /app/web/ --server "localhost:${PORT}" /app/web/index.php
106 | ```
107 |
108 |
109 | ### Local environment
110 |
111 | How to run this application in an Apache, NginX, or other popular HTTP servers
112 | falls outside the scope of this project.
113 |
114 | For development purposes, the internal PHP HTTP server _is_ explained below.
115 |
116 | #### Built-in PHP HTTP server
117 |
118 | For development purposes a Composer `serve-dev` command has been provided. This will
119 | run the application using PHP internal HTTP server.
120 |
121 | To use it, run `composer serve-dev` in the project root.
122 |
123 | **!!! FOR SECURITY REASONS, DO NOT USE THIS METHOD IN PRODUCTION !!!**
124 |
125 | By default, the application is hosted on `localhost` port `8080`.
126 | So if you visit http://localhost:8080/ with your browser, you should see "Hello, World!".
127 |
128 | Both the `HOST` and `PORT` can be configured before running the command by
129 | setting them in the environment, for instance:
130 |
131 | ```sh
132 | HOST='solid.local' PORT=1234 composer serve-dev
133 | ```
134 |
135 | This command can also be run through a docker container, for instance:
136 |
137 | ```
138 | export PORT=8080 && \
139 | docker run \
140 | --env "PORT=${PORT}" \
141 | --expose "${PORT}" \
142 | --network host \
143 | --rm \
144 | --volume "$PWD:/app" \
145 | -it \
146 | composer:latest \
147 | serve
148 | ```
149 |
150 |
157 |
158 | ## Running solid/webid-provider-tests
159 | Due to https://github.com/pdsinterop/php-solid-server/issues/8 you should run, in one terminal window:
160 | ```sh
161 | HOST=127.0.0.1 composer serve-dev
162 | ```
163 | and in another you run the [webid-provider-test](https://github.com/solid/webid-provider-tests) as:
164 | ```sh
165 | SERVER_ROOT=http://localhost:8080 ./node_modules/.bin/jest test/surface/fetch-openid-config.test.ts
166 | ```
167 | The current `dev` branch of php-solid-server should pass roughly 7 out of 17 tests.
168 |
169 | ## Available Features
170 |
171 | Based on the specifications, the features listed below _should_ be available.
172 |
173 | The checkboxes show which features _are_, and which ones _are not_.
174 |
175 | The underlying functionality for these features is provided by:
176 |
177 | - [auth] = [`pdsinterop/solid-auth`](https://github.com/pdsinterop/php-solid-auth)
178 | - [crud] = [`pdsinterop/solid-crud`](https://github.com/pdsinterop/php-solid-crud)
179 | - [p/s] = [`pdsinterop/solid-pubsub-server`](https://github.com/pdsinterop/php-solid-pubsub-server)
180 | - [rdf] = [`pdsinterop/flysystem-rdf`](https://github.com/pdsinterop/flysystem-rdf)
181 |
182 | 1. User
183 | - [x] Authentication [auth] (since **v0.3**)
184 | - [x] Identity (since **v0.2**)
185 | - [x] Profiles (since **v0.2**)
186 | 2. Data storage
187 | - [x] Content representation [rdf] (since **v0.4**)
188 | - [x] Resource API
189 | - [x] HTTP REST API [crud] (since **v0.4**)
190 | - [x] Websocket API [p/s] (since **v0.6**)
191 | 3. Web Acces Control List
192 | - [x] Authorization (and Access Control) [crud] (since **v0.6**)
193 | 4. Social web apps
194 | - [ ] Calendar
195 | - [ ] Contacts
196 | - [ ] Friends Lists (Followers, Following)
197 | - [ ] Notifications
198 |
199 | ## Development
200 |
201 | The easiest way to develop this project is by running the environment provided
202 | by the `docker-compose.yml` file. This can be done by running `docker-compose up`.
203 |
204 | This will start the application and a pubsub server in separate docker containers.
205 |
206 | ### Project structure
207 |
208 | This project is structured as follows:
209 |
210 | ```
211 | .
212 | ├── bin/ <- CLI scripts
213 | ├── config/ <- Empty directory where server configuration is generated
214 | ├── docs/ <- Documentation
215 | ├── src/ <- Source code
216 | ├── tests/ <- Test fixtures, Integration- and unit-tests
217 | ├── vendor/ <- Third-party and vendor code
218 | ├── web/ <- Web content
219 | ├── composer.json <- PHP package and dependency configuration
220 | └── README.md <- You are now here
221 | ```
222 |
223 | ### Testing
224 |
225 | The PHPUnit version to be used is the one installed as a `dev-` dependency via composer. It can be run using `composer test` or by calling it directly:
226 |
227 | ```sh
228 | $ ./bin/phpunit
229 | ```
230 |
231 | ## Contributing
232 |
233 | Questions or feedback can be given by [opening an issue on GitHub][issues-link].
234 |
235 | All PDS Interop projects are open source and community-friendly.
236 | Any contribution is welcome!
237 | For more details read the [contribution guidelines][contributing-link].
238 |
239 | All PDS Interop projects adhere to [the Code Manifesto](http://codemanifesto.com)
240 | as its [code-of-conduct][code-of-conduct]. Contributors are expected to abide by its terms.
241 |
242 | There is [a list of all contributors on GitHub][contributors-page].
243 |
244 | For a list of changes see the [CHANGELOG][changelog] or [the GitHub releases page][releases-page].
245 |
246 | ## License
247 |
248 | All code created by PDS Interop is licensed under the [MIT License][license-link].
249 |
250 | [changelog]: CHANGELOG.md
251 | [code-of-conduct]: CODE_OF_CONDUCT.md
252 | [contributing-link]: CONTRIBUTING.md
253 | [contributors-page]: https://github.com/pdsinterop/php-solid-server/contributors
254 | [issues-link]: https://github.com/pdsinterop/php-solid-server/issues
255 | [releases-page]: https://github.com/pdsinterop/php-solid-server/releases
256 | [keep-a-changelog-link]: https://keepachangelog.com/
257 | [keep-a-changelog-shield]: https://img.shields.io/badge/Keep%20a%20Changelog-f15d30.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHZpZXdCb3g9IjAgMCAxODcgMTg1Ij48cGF0aCBkPSJNNjIgN2MtMTUgMy0yOCAxMC0zNyAyMmExMjIgMTIyIDAgMDAtMTggOTEgNzQgNzQgMCAwMDE2IDM4YzYgOSAxNCAxNSAyNCAxOGE4OSA4OSAwIDAwMjQgNCA0NSA0NSAwIDAwNiAwbDMtMSAxMy0xYTE1OCAxNTggMCAwMDU1LTE3IDYzIDYzIDAgMDAzNS01MiAzNCAzNCAwIDAwLTEtNWMtMy0xOC05LTMzLTE5LTQ3LTEyLTE3LTI0LTI4LTM4LTM3QTg1IDg1IDAgMDA2MiA3em0zMCA4YzIwIDQgMzggMTQgNTMgMzEgMTcgMTggMjYgMzcgMjkgNTh2MTJjLTMgMTctMTMgMzAtMjggMzhhMTU1IDE1NSAwIDAxLTUzIDE2bC0xMyAyaC0xYTUxIDUxIDAgMDEtMTItMWwtMTctMmMtMTMtNC0yMy0xMi0yOS0yNy01LTEyLTgtMjQtOC0zOWExMzMgMTMzIDAgMDE4LTUwYzUtMTMgMTEtMjYgMjYtMzMgMTQtNyAyOS05IDQ1LTV6TTQwIDQ1YTk0IDk0IDAgMDAtMTcgNTQgNzUgNzUgMCAwMDYgMzJjOCAxOSAyMiAzMSA0MiAzMiAyMSAyIDQxLTIgNjAtMTRhNjAgNjAgMCAwMDIxLTE5IDUzIDUzIDAgMDA5LTI5YzAtMTYtOC0zMy0yMy01MWE0NyA0NyAwIDAwLTUtNWMtMjMtMjAtNDUtMjYtNjctMTgtMTIgNC0yMCA5LTI2IDE4em0xMDggNzZhNTAgNTAgMCAwMS0yMSAyMmMtMTcgOS0zMiAxMy00OCAxMy0xMSAwLTIxLTMtMzAtOS01LTMtOS05LTEzLTE2YTgxIDgxIDAgMDEtNi0zMiA5NCA5NCAwIDAxOC0zNSA5MCA5MCAwIDAxNi0xMmwxLTJjNS05IDEzLTEzIDIzLTE2IDE2LTUgMzItMyA1MCA5IDEzIDggMjMgMjAgMzAgMzYgNyAxNSA3IDI5IDAgNDJ6bS00My03M2MtMTctOC0zMy02LTQ2IDUtMTAgOC0xNiAyMC0xOSAzN2E1NCA1NCAwIDAwNSAzNGM3IDE1IDIwIDIzIDM3IDIyIDIyLTEgMzgtOSA0OC0yNGE0MSA0MSAwIDAwOC0yNCA0MyA0MyAwIDAwLTEtMTJjLTYtMTgtMTYtMzEtMzItMzh6bS0yMyA5MWgtMWMtNyAwLTE0LTItMjEtN2EyNyAyNyAwIDAxLTEwLTEzIDU3IDU3IDAgMDEtNC0yMCA2MyA2MyAwIDAxNi0yNWM1LTEyIDEyLTE5IDI0LTIxIDktMyAxOC0yIDI3IDIgMTQgNiAyMyAxOCAyNyAzM3MtMiAzMS0xNiA0MGMtMTEgOC0yMSAxMS0zMiAxMXptMS0zNHYxNGgtOFY2OGg4djI4bDEwLTEwaDExbC0xNCAxNSAxNyAxOEg5NnoiLz48L3N2Zz4K
258 | [license-link]: ./LICENSE
259 | [license-shield]: https://img.shields.io/github/license/pdsinterop/php-solid-server.svg
260 | [maintained-shield]: https://img.shields.io/maintenance/yes/2022.svg
261 | [pdsinterop-shield]: https://img.shields.io/badge/-PDS%20Interop-7C4DFF.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii01IC01IDExMCAxMTAiIGZpbGw9IiNGRkYiIHN0cm9rZS13aWR0aD0iMCI+CiAgICA8cGF0aCBkPSJNLTEgNTJoMTdhMzcuNSAzNC41IDAgMDAyNS41IDMxLjE1di0xMy43NWEyMC43NSAyMSAwIDAxOC41LTQwLjI1IDIwLjc1IDIxIDAgMDE4LjUgNDAuMjV2MTMuNzVhMzcgMzQuNSAwIDAwMjUuNS0zMS4xNWgxN2EyMiAyMS4xNSAwIDAxLTEwMiAweiIvPgogICAgPHBhdGggZD0iTSAxMDEgNDhhMi43NyAyLjY3IDAgMDAtMTAyIDBoIDE3YTIuOTcgMi44IDAgMDE2OCAweiIvPgo8L3N2Zz4K
262 | [pdsinterop-site]: https://pdsinterop.org/
263 | [project-stage-badge: Development]: https://img.shields.io/badge/Project%20Stage-Development-yellowgreen.svg
264 | [project-stage-page]: https://blog.pother.ca/project-stages/
265 | [standard-readme-link]: https://github.com/RichardLitt/standard-readme
266 | [standard-readme-shield]: https://img.shields.io/badge/-Standard%20Readme-brightgreen.svg
267 | [version-link]: https://packagist.org/packages/pdsinterop/php-solid-server
268 | [version-shield]: https://img.shields.io/github/v/release/pdsinterop/php-solid-server?sort=semver
269 |
--------------------------------------------------------------------------------