Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,34 @@ For the user management, no framework is used to keep the codebase lean.
Start the docker containers:

```sh
docker-compose up
docker compose up
```

This will start up three containers: the solid server, pubsub server and a mailpit server. If you have an actual SMTP server running, feel free to remove the mailpit container.
The persisted data will be stored in the data/ directory. This contains the keys, pods, db and mailpit data.

Run the following commands to set up the container (replace 'solid' below with the name of your container):
Note: Update the values in the config.php file where needed befure running the init script.
Run the following commands to set up the container:
Note: Update the values in the config.php file where needed before running the init script.

```sh
docker exec -w /opt/solid/ solid cp config.php.example config.php
docker exec -u www-data -i -w /opt/solid/ solid php init.php
docker exec -w /opt/solid/ solid chown -R www-data:www-data keys pods profiles db
docker compose exec -w /opt/solid/ solid cp config.php.example config.php
docker compose exec -w /opt/solid/ solid chown -R www-data:www-data keys pods profiles db
docker compose exec -u www-data -i -w /opt/solid/ solid php init.php
```

If you need dev user accounts bob and alice, also run this:

```sh
docker compose exec -u www-data -i -w /opt/solid/ solid php init-devusers.php
docker compose exec -w /opt/solid/ solid chown -R www-data:www-data keys pods profiles db # again
```

Now add the following host to your `/etc/hosts` file:
```
127.0.0.1 solid.local
```

And browser to `https://solid.local/`. After you register a new account, you'll get an identity and storage hostname, add these to `/etc/hosts` as well, e.g:
And browse to `https://solid.local/`. After you register a new account, you'll get an identity and storage hostname, add these to `/etc/hosts` as well, e.g:
```
127.0.0.1 id-d1f0e8c54e755cb45b61ee8e9dad00fe.solid.local storage-d1f0e8c54e755cb45b61ee8e9dad00fe.solid.local
```
Expand Down
5 changes: 4 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3'
services:
solid:
build:
Expand All @@ -11,6 +10,10 @@ services:
- ./data/db:/opt/solid/db
- ./data/pods:/opt/solid/pods
- ./data/profiles:/opt/solid/profiles
- ./frontend:/opt/solid/frontend

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a developer thing, but should not be in the default docker compose. Only the data (and maybe config) should persist to the host by default

- ./lib:/opt/solid/lib
- ./www/idp:/opt/solid/www/idp
- ./config.php:/opt/solid/config.php
pubsub:
build:
context: https://github.com/pdsinterop/php-solid-pubsub-server.git#main
Expand Down
50 changes: 50 additions & 0 deletions init-devusers.php

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can be removed - there is already a script for this in the testsuite directory that can be reused, it already creates the 'alice' and 'bob' accounts.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
require_once(__DIR__ . "/config.php");
require_once(__DIR__ . "/vendor/autoload.php");

function addStorage($userId, $ownerWebId) {
$storageId = $userId;
try {
$pdo = new \PDO("sqlite:" . DBPATH);
$query = $pdo->prepare('INSERT INTO storage VALUES(:storageId, :owner)');
$query->execute([
':storageId' => $storageId,
':owner' => $ownerWebId
]);
} catch(\PDOException $e) {
echo $e->getMessage();
}
}

function addUser($userId) {
try {
$pdo = new \PDO("sqlite:" . DBPATH);
$query = $pdo->prepare('INSERT INTO users VALUES (:userId, :email, :passwordHash, :data)');
$webId = "https://id-" . $userId . "." . BASEDOMAIN . "/#me";
$userData = [
"id" => $userId,
"email" => $userId,
"webId" => $webId
];
$query->execute([
':userId' => $userId,
':email' => $userId,
':passwordHash' => password_hash($userId, PASSWORD_BCRYPT),
':data' => json_encode($userData)
]);
} catch(\PDOException $e) {
echo $e->getMessage();
}
return $webId;
}

$users = [
'alice',
'bob'
];

foreach ($users as $user) {
$webId = addUser($user);
addStorage($user, $webId);
}

2 changes: 1 addition & 1 deletion lib/Routes/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public static function respondToAccountNew() {
header("HTTP/1.1 400 Bad Request");
exit();
}
if (!$_POST['password'] === $_POST['repeat_password']) {
if ($_POST['password'] !== $_POST['repeat_password']) {
error_log("Password repeat does not match");
header("HTTP/1.1 400 Bad Request");
exit();
Expand Down
93 changes: 14 additions & 79 deletions lib/Routes/SolidStorage.php

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this file split up? This change is adding a layer of indirection I think we don't need to add. This change reduces the file to a glue layer, but we already have a glue layer www directory.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so that I can write unit tests for the SolidStorageHandler, which accepts request/response objects. This avoids adding complex mocks/stubs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the www directory, would it be a good idea to move the construction of the request/response objects there, and pass them to all the separate route handlers? That fixes the problem without adding another layer.

Original file line number Diff line number Diff line change
@@ -1,86 +1,21 @@
<?php
namespace Pdsinterop\PhpSolid\Routes;

use Pdsinterop\PhpSolid\User;
use Pdsinterop\PhpSolid\StorageServer;
use Pdsinterop\PhpSolid\ClientRegistration;
use Pdsinterop\PhpSolid\SolidNotifications;
use Pdsinterop\PhpSolid\Util;
use Pdsinterop\Solid\Auth\WAC;
use Pdsinterop\Solid\Resources\Server as ResourceServer;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Response;
namespace Pdsinterop\PhpSolid\Routes;

class SolidStorage {
public static function respondToStorage() {
$requestFactory = new ServerRequestFactory();
$rawRequest = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES);
use Laminas\Diactoros\ServerRequestFactory;
use Pdsinterop\PhpSolid\SolidStorageHandler;
use Pdsinterop\PhpSolid\StorageServer;

try {
StorageServer::initializeStorage();
$filesystem = StorageServer::getFileSystem();
} catch (\Exception $e) {
$response = new Response();
$response = $response->withStatus(404, "Not found");
StorageServer::respond($response);
exit();
}
class SolidStorage
{
public static function respondToStorage()
{
$requestFactory = new ServerRequestFactory();
$rawRequest = $requestFactory->fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES);

$resourceServer = new ResourceServer($filesystem, new Response(), null);
$solidNotifications = new SolidNotifications();
$resourceServer->setNotifications($solidNotifications);
$handler = new SolidStorageHandler();
$response = $handler->handle($rawRequest);

$wac = new WAC($filesystem);

$baseUrl = Util::getServerBaseUrl();
$resourceServer->setBaseUrl($baseUrl);
$wac->setBaseUrl($baseUrl);

try {
$webId = StorageServer::getWebId($rawRequest);
} catch(\Exception $e) {
$response = $resourceServer->getResponse()
-> withStatus(400, "Bad request");
StorageServer::respond($response);
exit();
}

if (!isset($webId)) {
$response = $resourceServer->getResponse()
->withStatus(409, "Invalid token");
StorageServer::respond($response);
exit();
}

$origin = $rawRequest->getHeaderLine("Origin");

// FIXME: Read allowed clients from the profile instead;

$ownerWebId = StorageServer::getOwnerWebId();
$owner = User::getUserByWebId($ownerWebId);

$allowedClients = $owner['allowedClients'] ?? [];
$allowedOrigins = array_merge(
($owner['allowedOrigins'] ?? []),
(TRUSTED_APPS ?? [])
);
$allowedOrigins = array_unique($allowedOrigins);

if (!isset($origin) || ($origin === "")) {
$allowedOrigins[] = "app://unset"; // FIXME: this should not be here.
$origin = "app://unset";
}

if (!$wac->isAllowed($rawRequest, $webId, $origin, $allowedOrigins)) {
$response = new Response();
$response = $response->withStatus(403, "Access denied!");
StorageServer::respond($response);
exit();
}

$response = $resourceServer->respondToRequest($rawRequest);
$response = $wac->addWACHeaders($rawRequest, $response, $webId);
StorageServer::respond($response);
}
StorageServer::respond($response);
}
}
7 changes: 6 additions & 1 deletion lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ public static function getConfigClient() {
if ($clientId) {
$registeredClient = ClientRegistration::getRegistration($clientId);
}
if (isset($registeredClient)) {
if (isset($registeredClient)) {
if (!$registeredClient || !isset($registeredClient['redirect_uris'])) {
//TODO: better to throw an error and handle that on the outside
header("HTTP/1.1 400 Bad request");
exit();
}
return new ConfigClient(
$clientId,
$registeredClient['client_secret'] ?? '',
Expand Down
71 changes: 71 additions & 0 deletions lib/SolidStorageHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

namespace Pdsinterop\PhpSolid;

use Laminas\Diactoros\Response;
use Pdsinterop\Solid\Auth\WAC;
use Pdsinterop\Solid\Resources\Server as ResourceServer;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class SolidStorageHandler
{
public function handle(ServerRequestInterface $rawRequest): ResponseInterface
{
try {
StorageServer::initializeStorage();
$filesystem = StorageServer::getFileSystem();
} catch (\Exception $e) {
return (new Response())->withStatus(404, "Not found");
}

$resourceServer = new ResourceServer($filesystem, new Response(), null);

$solidNotifications = new SolidNotifications();
$resourceServer->setNotifications($solidNotifications);

$wac = new WAC($filesystem);

$baseUrl = Util::getServerBaseUrl();
$resourceServer->setBaseUrl($baseUrl);
$wac->setBaseUrl($baseUrl);

try {
$webId = StorageServer::getWebId($rawRequest);
} catch (\Exception $e) {
return $resourceServer->getResponse()
->withStatus(400, "Bad request");
}

if (!isset($webId)) {
return $resourceServer->getResponse()
->withStatus(409, "Invalid token");
}

$origin = $rawRequest->getHeaderLine("Origin");

// FIXME: Read allowed clients from the profile instead;
$ownerWebId = StorageServer::getOwnerWebId();
$owner = User::getUserByWebId($ownerWebId);
$allowedClients = $owner['allowedClients'] ?? [];

$allowedOrigins = array_merge(
($owner['allowedOrigins'] ?? []),
(TRUSTED_APPS ?? [])
);
$allowedOrigins = array_unique($allowedOrigins);

if (!isset($origin) || ($origin === "")) {
$allowedOrigins[] = "app://unset"; // FIXME: this should not be here.
$origin = "app://unset";
}

if (!$wac->isAllowed($rawRequest, $webId, $origin, $allowedOrigins)) {
return (new Response())->withStatus(403, "Access denied!");
}

$response = $resourceServer->respondToRequest($rawRequest);

return $wac->addWACHeaders($rawRequest, $response, $webId);
}
}
Loading