From 61203001a34834737d38d1608805ab05305abd62 Mon Sep 17 00:00:00 2001 From: Sascha Nitsch Date: Tue, 23 Jul 2024 22:37:02 +0200 Subject: [PATCH] support for user discovery endpoint --- README.md | 15 ++- php/federator/api.php | 3 + php/federator/api/fedusers.php | 117 ++++++++++++++++++++++++ php/federator/data/user.php | 86 ++++++++++++++++- php/federator/dio/user.php | 96 +++++++++++++++---- plugins/federator/contentnation.php | 18 +++- plugins/federator/dummyconnector.php | 2 - sql/2024-07-23.sql | 9 ++ templates/federator/user.json | 104 +++++++++++++++++++++ templates/federator/webfinger_acct.json | 5 +- 10 files changed, 424 insertions(+), 31 deletions(-) create mode 100644 php/federator/api/fedusers.php create mode 100644 sql/2024-07-23.sql create mode 100644 templates/federator/user.json diff --git a/README.md b/README.md index 4598b36..e4c8412 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,10 @@ Needed SQL commands: create user if not exists 'federatoradmin'@'localhost' identified by '*change*me*as*well'; grant all privileges on federator.* to 'federatoradmin'@'localhost'; -This will be changed, but works for the current develop verison. +After this, change the settings in the ini file to match above changed passwords. +change to php/federator and run + php maintenance dbupgrade +to install all the needed table. Also run this after an update. If the include redis cache is enabled, create a users.acl for redis with the content: @@ -38,8 +41,15 @@ To configure an apache server, add the following rewrite rules: RewriteEngine on RewriteBase / - RewriteRule ^api/federator/(.+)$ federator.php?_call=$1 [L] + RewriteCond expr "%{HTTP:accept} -strcmatch '*application/ld+json*'" [OR] + RewriteCond expr "%{HTTP:accept} -strcmatch '*application/jrd+json*'" [OR] + RewriteCond expr "%{HTTP:accept} -strcmatch '*application/activity+json*'" [OR] + RewriteCond expr "%{HTTP:content-type} -strcmatch '*application/activity+json*'" + RewriteRule ^@(.*)$ /federator.php?_call=fedusers/$1 [L,END] + RewriteRule ^users/(.*)$ /federator.php?_call=fedusers/$1 [L,END] + RewriteRule ^api/federator/(.+)$ federator.php?_call=$1 [L,END] RewriteRule ^(\.well-known/.*)$ /federator.php?_call=$1 [L,END] + RewriteRule ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END] With the dummy plugin and everything installed correctly a @@ -47,4 +57,3 @@ With the dummy plugin and everything installed correctly a > curl -v http://localhost/api/federator/v1/dummy/moo -H "X-Session: somethingvalid" -H "X-Profile: ihaveone" should return a piece of ascii art. - diff --git a/php/federator/api.php b/php/federator/api.php index 5134850..83ff2e2 100644 --- a/php/federator/api.php +++ b/php/federator/api.php @@ -98,6 +98,9 @@ class Api extends Main case 'nodeinfo': $handler = new Api\WellKnown($this); break; + case 'fedusers': + $handler = new Api\FedUsers($this); + break; case 'v1': switch ($this->paths[1]) { case 'dummy': diff --git a/php/federator/api/fedusers.php b/php/federator/api/fedusers.php new file mode 100644 index 0000000..e94517b --- /dev/null +++ b/php/federator/api/fedusers.php @@ -0,0 +1,117 @@ +main = $main; + } + + /** + * run given url path + * + * @param array $paths path array split by / + * @param \Federator\Data\User|false $user user who is calling us @unused-param + * @return bool true on success + */ + public function exec($paths, $user) + { + $method = $_SERVER["REQUEST_METHOD"]; + switch ($method) { + case 'GET': + switch (sizeof($paths)) { + case 2: + // /users/username or /@username + return $this->returnUserProfile($paths[1]); + } + break; + } + $this->main->setResponseCode(404); + return false; + } + + + /** + * return user profile + * + * @param string $_name + * @return boolean true on success + */ + private function returnUserProfile($_name) + { + $user = \Federator\DIO\User::getUserByName( + $this->main->getDatabase(), + $_name, + $this->main->getConnector(), + $this->main->getCache() + ); + if ($user === false || $user->id === null) { + throw new \Federator\Exceptions\FileNotFound(); + } + $data = [ + 'iconMediaType' => $user->iconMediaType, + 'iconURL' => $user->iconURL, + 'imageMediaType' => $user->imageMediaType, + 'imageURL' => $user->imageURL, + 'fqdn' => $_SERVER['SERVER_NAME'], + 'name' => $user->name, + 'username' => $user->id, + 'publickey' => str_replace("\n", "\\n", $user->publicKey), + 'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z + 'summary' => $user->summary, + 'type' => $user->type + ]; + $this->response = $this->main->renderTemplate('user.json', $data); + return true; + } + /** + * set response + * + * @param string $response response to set + * @return void + */ + public function setResponse($response) + { + $this->response = $response; + } + + /** + * get internal represenation as json string + * @return string json string or html + */ + public function toJson() + { + return $this->response; + } +} diff --git a/php/federator/data/user.php b/php/federator/data/user.php index bbc7015..efb829d 100644 --- a/php/federator/data/user.php +++ b/php/federator/data/user.php @@ -20,6 +20,20 @@ class User */ public $externalid; + /** + * icon media type + * + * @var string $iconMediaType + */ + public $iconMediaType; + + /** + * icon url + * + * @var string $iconURL + */ + public $iconURL; + /** * user id * @@ -27,9 +41,29 @@ class User */ public $id; + /** + * image media type + * + * @var string $imageMediaType + */ + public $imageMediaType; + + /** + * image url + * + * @var string $imageURL + */ + public $imageURL; + /* @var string user language */ //public $lang; + /** + * user name + * + * @var string $name + */ + public $name = ''; /** * user permissions * @@ -38,12 +72,40 @@ class User */ public $permissions = []; + /** + * user public key + * + * @var string $publicKey + */ + public $publicKey; + + /** + * registered unix timestamp + * + * @var int $registered + */ + public $registered = 0; + /** * session id * * @var string $session * */ - public $session; + public $session = ''; + + /** + * summary for user/profile + * + * @var string $summary + */ + public $summary = ''; + + /** + * type of user (user/group) + * + * @var string $type + */ + public $type = 'group'; /** * create new user object from json string @@ -58,10 +120,18 @@ class User return false; } $user = new User(); + $user->iconMediaType = $data['iconMediaType']; + $user->iconURL = $data['iconURL']; $user->id = $data['id']; + $user->imageMediaType = $data['imageMediaType']; + $user->imageURL = $data['imageURL']; $user->externalid = $data['externalid']; + $user->name = $data['name']; /// TODO: replace with enums $user->permissions = $data['permissions']; + $user->publicKey = $data['publicKey']; + $user->summary = $data['summary']; + $user->type = $data['type']; return $user; } @@ -86,9 +156,17 @@ class User public function toJson() { $data = [ - 'id' => $this->id, - 'externalid' => $this->externalid, - 'permissions' => $this->permissions + 'iconMediaType' => $this->iconMediaType, + 'iconURL' => $this->iconURL, + 'id' => $this->id, + 'imageMediaType' => $this->iconMediaType, + 'imageURL' => $this->iconURL, + 'externalid' => $this->externalid, + 'name' => $this->name, + 'permissions' => $this->permissions, + 'publicKey' => $this->publicKey, + 'summary' => $this->summary, + 'type' => $this->type ]; return json_encode($data) | ''; } diff --git a/php/federator/dio/user.php b/php/federator/dio/user.php index a625959..82084fa 100644 --- a/php/federator/dio/user.php +++ b/php/federator/dio/user.php @@ -22,27 +22,78 @@ class User */ protected static function addLocalUser($dbh, $user, $_user) { - // needed fields: RSA key pair, user name (handle) - $private_key = openssl_pkey_new(); - if ($private_key === false) { + // check if it is timed out user + $sql = 'select unix_timestamp(`validuntil`) from users where id=?'; + $stmt = $dbh->prepare($sql); + if ($stmt === false) { throw new \Federator\Exceptions\ServerError(); } - $public = openssl_pkey_get_details($private_key)['key']; - $private = ''; - openssl_pkey_export($private_key, $private); - try { - $sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil)'; - $sql .= ' values (?, ?, ?, ?, now() + interval 1 day)'; - $sql .= ' on duplicate key update validuntil=now() + interval 1 day'; + $stmt->bind_param("s", $_user); + $validuntil = 0; + $ret = $stmt->bind_result($validuntil); + $stmt->execute(); + if ($ret) { + $stmt->fetch(); + } + $stmt->close(); + if ($validuntil == 0) { + $private_key = openssl_pkey_new(); + if ($private_key === false) { + throw new \Federator\Exceptions\ServerError(); + } + $public = openssl_pkey_get_details($private_key)['key']; + $private = ''; + openssl_pkey_export($private_key, $private); + $sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil, type, name, summary, registered, iconmediatype, iconurl, imagemediatype, imageurl)'; + $sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $dbh->prepare($sql); if ($stmt === false) { throw new \Federator\Exceptions\ServerError(); } - $stmt->bind_param("ssss", $_user, $user->externalid, $public, $private); + $registered = gmdate('Y-m-d H:i:s', $user->registered); + $stmt->bind_param( + "ssssssssssss", + $_user, + $user->externalid, + $public, + $private, + $user->type, + $user->name, + $user->summary, + $registered, + $user->iconMediaType, + $user->iconURL, + $user->imageMediaType, + $user->imageURL + ); + } else { + // update to existing user + $sql = 'update users set validuntil=now() + interval 1 day, type=?, name=?, summary=?, registered=?, iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?'; + $stmt = $dbh->prepare($sql); + if ($stmt === false) { + throw new \Federator\Exceptions\ServerError(); + } + $registered = gmdate('Y-m-d H:i:s', $user->registered); + $stmt->bind_param( + "sssssssss", + $user->type, + $user->name, + $user->summary, + $registered, + $user->iconMediaType, + $user->iconURL, + $user->imageMediaType, + $user->imageURL, + $_user + ); + } + try { $stmt->execute(); $stmt->close(); $user->id = $_user; } catch (\mysqli_sql_exception $e) { + error_log($sql); + error_log(print_r($user, true)); error_log($e->getMessage()); } } @@ -55,20 +106,21 @@ class User */ protected static function extendUser(\mysqli $dbh, $user, $_user) : void { - $sql = 'select id from users where id=?'; + $sql = 'select id,unix_timestamp(`validuntil`) from users where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { throw new \Federator\Exceptions\ServerError(); } $stmt->bind_param("s", $_user); - $ret = $stmt->bind_result($user->id); + $validuntil = 0; + $ret = $stmt->bind_result($user->id, $validuntil); $stmt->execute(); if ($ret) { $stmt->fetch(); } $stmt->close(); // if a new user, create own database entry with additionally needed info - if ($user->id === null) { + if ($user->id === null || $validuntil < time()) { self::addLocalUser($dbh, $user, $_user); } @@ -99,14 +151,26 @@ class User return $user; } // check our db - $sql = 'select id,externalid from users where id=? and validuntil>=now()'; + $sql = 'select id,externalid,type,name,summary,unix_timestamp(registered),rsapublic,iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()'; $stmt = $dbh->prepare($sql); if ($stmt === false) { throw new \Federator\Exceptions\ServerError(); } $stmt->bind_param("s", $_name); $user = new \Federator\Data\User(); - $ret = $stmt->bind_result($user->id, $user->externalid); + $ret = $stmt->bind_result( + $user->id, + $user->externalid, + $user->type, + $user->name, + $user->summary, + $user->registered, + $user->publicKey, + $user->iconMediaType, + $user->iconURL, + $user->imageMediaType, + $user->imageURL, + ); $stmt->execute(); if ($ret) { $stmt->fetch(); diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index 05855d6..95f649c 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -83,11 +83,16 @@ class ContentNation implements Connector if ($r === false || $r === null || !is_array($r)) { return false; } - if (!array_key_exists('name', $r) || $r['name'] !== $_name) { - return false; - } $user = new \Federator\Data\User(); $user->externalid = $_name; + $user->iconMediaType = $r['iconMediaType']; + $user->iconURL = $r['iconURL']; + $user->imageMediaType = $r['imageMediaType']; + $user->imageURL = $r['imageURL']; + $user->name = $r['name']; + $user->summary = $r['summary']; + $user->type = $r['type']; + $user->registered = intval($r['registered'], 10); return $user; } @@ -118,8 +123,11 @@ class ContentNation implements Connector if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) { return false; } - $user = new \Federator\Data\User(); - $user->externalid = $_user; + $user = $this->getRemoteUserByName($_user); + if ($user === false) { + return false; + } + // extend with permissions $user->permissions = []; $user->session = $_session; foreach ($r[$_user] as $p) { diff --git a/plugins/federator/dummyconnector.php b/plugins/federator/dummyconnector.php index 9aef028..ee98424 100644 --- a/plugins/federator/dummyconnector.php +++ b/plugins/federator/dummyconnector.php @@ -43,8 +43,6 @@ class DummyConnector implements Connector // validate $_session and $user $user = new \Federator\Data\User(); $user->externalid = $_name; - $user->permissions = []; - $user->session = ''; return $user; } diff --git a/sql/2024-07-23.sql b/sql/2024-07-23.sql new file mode 100644 index 0000000..04aff80 --- /dev/null +++ b/sql/2024-07-23.sql @@ -0,0 +1,9 @@ +alter table users add `type` enum('person', 'group') default 'person'; +alter table users add `name` varchar(255) default ''; +alter table users add `summary` text default ''; +alter table users add `registered` timestamp default 0; +alter table users add `iconmediatype` varchar(255) default ''; +alter table users add `iconurl` varchar(255) default ''; +alter table users add `imagemediatype` varchar(255) default ''; +alter table users add `imageurl` varchar(255) default ''; +update settings set `value`="2024-07-23" where `key`="database_version"; diff --git a/templates/federator/user.json b/templates/federator/user.json new file mode 100644 index 0000000..752eb80 --- /dev/null +++ b/templates/federator/user.json @@ -0,0 +1,104 @@ +{ldelim} + "@context":[ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + {ldelim} + "manuallyApprovesFollowers":"as:manuallyApprovesFollowers", + "toot":"http://joinmastodon.org/ns#", + "featured":{ldelim} + "@id":"toot:featured", + "@type":"@id" + {rdelim}, + "featuredTags":{ldelim} + "@id":"toot:featuredTags", + "@type":"@id" + {rdelim}, + "alsoKnownAs":{ldelim} + "@id":"as:alsoKnownAs", + "@type":"@id" + {rdelim}, + "movedTo":{ldelim} + "@id":"as:movedTo", + "@type":"@id" + {rdelim}, + "schema":"http://schema.org#", + "PropertyValue":"schema:PropertyValue", + "value":"schema:value", + "discoverable":"toot:discoverable", + "Device":"toot:Device", + "Ed25519Signature":"toot:Ed25519Signature", + "Ed25519Key":"toot:Ed25519Key", + "Curve25519Key":"toot:Curve25519Key", + "EncryptedMessage":"toot:EncryptedMessage", + "publicKeyBase64":"toot:publicKeyBase64", + "deviceId":"toot:deviceId", + "claim":{ldelim} + "@type":"@id", + "@id":"toot:claim" + {rdelim}, + "fingerprintKey":{ldelim} + "@type":"@id", + "@id":"toot:fingerprintKey" + {rdelim}, + "identityKey":{ldelim} + "@type":"@id", + "@id":"toot:identityKey" + {rdelim}, + "devices":{ldelim} + "@type":"@id", + "@id":"toot:devices" + {rdelim}, + "messageFranking":"toot:messageFranking", + "messageType":"toot:messageType", + "cipherText":"toot:cipherText", + "suspended":"toot:suspended", + "focalPoint":{ldelim} + "@container":"@list", + "@id":"toot:focalPoint" + {rdelim} + {rdelim} + ], + "id":"https://{$fqdn}/users/{$username}", + "type":"{$type}", + "following":"https://{$fqdn}/users/{$username}/following", + "followers":"https://{$fqdn}/users/{$username}/followers", + "inbox":"https://{$fqdn}/users/{$username}/inbox", + "outbox":"https://{$fqdn}/users/{$username}/outbox", + "featured":"https://{$fqdn}/users/{$username}/collections/featured", + "featuredTags":"https://{$fqdn}/users/{$username}/collections/tags", + "preferredUsername":"{$username}", + "name":"{$name}", + "summary":"{$summary}", + "url":"https://{$fqdn}/@{$username}", + "manuallyApprovesFollowers":false, + "discoverable":true, + "published":"{$registered}", + "publicKey":{ldelim} + "id":"https://{$fqdn}/users/{$username}#main-key", + "owner":"https://{$fqdn}/users/{$username}", + "publicKeyPem":"{$publickey}" + {rdelim}, + "tag":[], + "attachment":[ + {if $type==='group'}{ldelim} + "type":"PropertyValue", + "name":"website", + "value":"\u003ca href=\"https://{$fqdn}/@{$username}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003e{$fqdn}/@{$username}\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" + {rdelim}{/if} + ], + "endpoints":{ldelim} + "sharedInbox":"https://{$fqdn}/inbox" + {rdelim}, +{if $iconURL !== ''} "icon":{ldelim} + "type":"Image", + "mediaType":"{$iconMediaType}", + "url":"{$iconURL}" + {rdelim}, +{/if} +{if $imageURL !== ''} "image":{ldelim} + "type":"Image", + "mediaType":"{$imageMediaType}", + "url":"{$imageURL}" + {rdelim} +{/if} +{rdelim} diff --git a/templates/federator/webfinger_acct.json b/templates/federator/webfinger_acct.json index 4777c44..ab532ce 100644 --- a/templates/federator/webfinger_acct.json +++ b/templates/federator/webfinger_acct.json @@ -1,6 +1,9 @@ {ldelim} "subject": "acct:{$username}@{$domain}", - "aliases": ["https://{$domain}/@{$username}"], + "aliases": [ + "https://{$domain}/@{$username}" + "https://{$domain}/users/{$username}" + ], "links": [ {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim}, {if $type=='Group'}