From 47efd74b6cf5b792a19712cd8d0d98b731b9a47c Mon Sep 17 00:00:00 2001 From: Sascha Nitsch Date: Sat, 20 Jul 2024 23:21:27 +0200 Subject: [PATCH] initial webfinger support --- README.md | 1 + config.ini | 5 +- php/federator/api.php | 46 +++++----- .../api/{v1.php => apiinterface.php} | 6 +- php/federator/api/v1/dummy.php | 9 +- php/federator/api/wellknown.php | 84 +++++++++++++++++++ php/federator/api/wellknown/webfinger.php | 66 +++++++++++++++ php/federator/dio/user.php | 27 ++++++ php/federator/main.php | 83 +++++++++++------- plugins/federator/contentnation.php | 83 ++++++++++++++++++ progress.md | 2 +- templates/federator/webfinger_acct.json | 11 +++ 12 files changed, 364 insertions(+), 59 deletions(-) rename php/federator/api/{v1.php => apiinterface.php} (79%) create mode 100644 php/federator/api/wellknown.php create mode 100644 php/federator/api/wellknown/webfinger.php create mode 100644 plugins/federator/contentnation.php create mode 100644 templates/federator/webfinger_acct.json diff --git a/README.md b/README.md index bccb773..58e5bd6 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ To configure an apache server, add the following rewrite rules: RewriteEngine on RewriteBase / RewriteRule ^api/(.+)$ api.php?_call=$1 [L] + RewriteRule ^(\.well-known/.*)$ /api.php?_call=$1 [L,END] With the dummy plugin and everything installed correctly a diff --git a/config.ini b/config.ini index d2f0e56..dcf9ba1 100644 --- a/config.ini +++ b/config.ini @@ -1,3 +1,6 @@ +[generic] +externaldomain = 'your.fqdn' + [database] host = '127.0.0.1' username = 'federator' @@ -5,7 +8,7 @@ password = '*change*me*' database = 'federator' [templates] -path = '../templates/' +path = '../templates/federator/' compiledir = '../cache' [plugins] diff --git a/php/federator/api.php b/php/federator/api.php index 5eb7c59..931771b 100644 --- a/php/federator/api.php +++ b/php/federator/api.php @@ -46,7 +46,6 @@ class Api extends Main */ public function __construct() { - $this->smarty = null; $this->contentType = "application/json"; Main::__construct(); } @@ -77,40 +76,39 @@ class Api extends Main $this->loadPlugins(); $retval = ""; $handler = null; - if (!array_key_exists('HTTP_X_SESSION', $_SERVER) || !array_key_exists('HTTP_X_PROFILE', $_SERVER)) { - http_response_code(403); - return; - } if ($this->connector === null) { http_response_code(500); return; } - $this->user = DIO\User::getUserBySession( - $this->dbh, - $_SERVER['HTTP_X_SESSION'], - $_SERVER['HTTP_X_PROFILE'], - $this->connector, - $this->cache - ); - if ($this->user === false) { - http_response_code(403); - return; + if (array_key_exists('HTTP_X_SESSION', $_SERVER) && array_key_exists('HTTP_X_PROFILE', $_SERVER)) { + $this->user = DIO\User::getUserBySession( + $this->dbh, + $_SERVER['HTTP_X_SESSION'], + $_SERVER['HTTP_X_PROFILE'], + $this->connector, + $this->cache + ); + if ($this->user === false) { + http_response_code(403); + return; + } } - switch ($this->path[0]) { - case 'v': - if ($this->paths[0] === "v1") { - switch ($this->paths[1]) { - case 'dummy': - $handler = new Api\V1\Dummy($this); - break; - } + switch ($this->paths[0]) { + case 'v1': + switch ($this->paths[1]) { + case 'dummy': + $handler = new Api\V1\Dummy($this); + break; } break; + case '.well-known': + $handler = new Api\WellKnown($this); + break; } $printresponse = true; if ($handler !== null) { try { - $printresponse = $handler->exec($this->paths); + $printresponse = $handler->exec($this->paths, $this->user); if ($printresponse) { $retval = $handler->toJson(); } diff --git a/php/federator/api/v1.php b/php/federator/api/apiinterface.php similarity index 79% rename from php/federator/api/v1.php rename to php/federator/api/apiinterface.php index a91eef0..b8cf7fe 100644 --- a/php/federator/api/v1.php +++ b/php/federator/api/apiinterface.php @@ -11,15 +11,17 @@ namespace Federator\Api; /** * API interface */ -interface V1 +interface APIInterface { /** * run given url path * * @param array $paths path array split by / + * + * @param \Federator\Data\User|false $user user who is calling us * @return bool true on success */ - public function exec($paths); + public function exec($paths, $user); /** * get internal represenation as json string diff --git a/php/federator/api/v1/dummy.php b/php/federator/api/v1/dummy.php index e92ab13..b70a5ac 100644 --- a/php/federator/api/v1/dummy.php +++ b/php/federator/api/v1/dummy.php @@ -11,7 +11,7 @@ namespace Federator\Api\V1; /** * dummy api class for functional poc */ -class Dummy implements \Federator\Api\V1 +class Dummy implements \Federator\Api\APIInterface { /** * \Federator\Main instance @@ -41,10 +41,15 @@ class Dummy implements \Federator\Api\V1 * run given url path * * @param array $paths path array split by / + * @param \Federator\Data\User|false $user user who is calling us * @return bool true on success */ - public function exec($paths) : bool + public function exec($paths, $user) : bool { + // only for user with the 'publish' permission + if ($user === false || $user->hasPermission('publish') === false) { + throw new \Federator\Exceptions\PermissionDenied(); + } $method = $_SERVER["REQUEST_METHOD"]; switch ($method) { case 'GET': diff --git a/php/federator/api/wellknown.php b/php/federator/api/wellknown.php new file mode 100644 index 0000000..95bccdd --- /dev/null +++ b/php/federator/api/wellknown.php @@ -0,0 +1,84 @@ +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: + if ($paths[1] === 'webfinger') { + $wf = new WellKnown\WebFinger($this, $this->main); + return $wf->exec(); + } + } + break; + } + $this->main->setResponseCode(404); + return false; + } + + /** + * 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/api/wellknown/webfinger.php b/php/federator/api/wellknown/webfinger.php new file mode 100644 index 0000000..8c1cc16 --- /dev/null +++ b/php/federator/api/wellknown/webfinger.php @@ -0,0 +1,66 @@ +wellKnown = $wellKnown; + $this->main = $main; + } + + /** + * handle webfinger request + * + * @return bool true on success + */ + public function exec() + { + $_resource = $this->main->extractFromURI('resource'); + $matches = []; + $config = $this->main->getConfig(); + $domain = $config['generic']['externaldomain']; + if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) { + throw new \Federator\Exceptions\InvalidArgument(); + } + $user = \Federator\DIO\User::getUserByName($this->main->getDatabase(), $matches[1]); + if ($user->id == 0) { + throw new \Federator\Exceptions\FileNotFound(); + } + $data = [ + 'username' => $user->id, + 'domain' => $domain, + ]; + $response = $this->main->renderTemplate('webfinger_acct.json', $data); + $this->wellKnown->setResponse($response); + return true; + } +} diff --git a/php/federator/dio/user.php b/php/federator/dio/user.php index d31db63..65fde70 100644 --- a/php/federator/dio/user.php +++ b/php/federator/dio/user.php @@ -70,6 +70,33 @@ class User // no further processing for now } + /** + * get user by name + * + * @param \mysqli $dbh + * database handle + * @param string $_name + * user name + * @return \Federator\Data\User + */ + public static function getUserByName($dbh, $_name) + { + $sql = 'select id from users where id=?'; + $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); + $stmt->execute(); + if ($ret) { + $stmt->fetch(); + } + $stmt->close(); + return $user; + } + /** * get User by session id * diff --git a/php/federator/main.php b/php/federator/main.php index 3c52abe..ffb6388 100644 --- a/php/federator/main.php +++ b/php/federator/main.php @@ -7,7 +7,9 @@ * @author Author: Sascha Nitsch (grumpydeveloper) **/ - namespace Federator; +namespace Federator; + +require_once($_SERVER['DOCUMENT_ROOT'] . '../vendor/autoload.php'); /** * Base class for Api and related classes @@ -51,30 +53,20 @@ class Main * @var array $headers */ protected $headers = []; - /** - * languange instance - * - * @var Language $lang - */ - protected $lang = null; + /** * redirect URL * * @var ?string $redirect */ protected $redirect = null; + /** * response code * * @var int $responseCode */ protected $responseCode = 200; - /** - * smarty instance - * - * @var \Smarty\Smarty|null $smarty - */ - protected $smarty; /** * constructor @@ -90,6 +82,29 @@ class Main } } + /** + * extract parameter from URI + * + * @param string $param + * parameter to extract + * @param string $fallback + * optional fallback + * @return string + */ + public static function extractFromURI($param, $fallback = '') + { + $uri = $_SERVER['REQUEST_URI']; + $params = substr($uri, (int)(strpos($uri, '?') + 1)); + $params = explode('&', $params); + foreach ($params as $p) { + $tokens = explode('=', $p); + if ($tokens[0] === $param) { + return urldecode($tokens[1]); + } + } + return $fallback; + } + /** * do a remote call and return results * @param string $remoteURL remote URL @@ -128,7 +143,7 @@ class Main /** * get database handle * - * @return \mysqli|false database handle + * @return \mysqli database handle */ public function getDatabase() { @@ -175,6 +190,27 @@ class Main } } + /** + * render template + * + * @param string $template template file to render + * @param array $data template variables + * @return string + */ + public function renderTemplate($template, $data) + { + $smarty = new \Smarty\Smarty(); + $root = $_SERVER['DOCUMENT_ROOT']; + $smarty->setCompileDir($root . $this->config['templates']['compiledir']); + $smarty->setTemplateDir((string)realpath($root . $this->config['templates']['path'])); + $smarty->assign('database', $this->dbh); + $smarty->assign('maininstance', $this); + foreach ($data as $key => $value) { + $smarty->assign($key, $value); + } + return $smarty->fetch($template); + } + /** * set cache */ @@ -215,20 +251,10 @@ class Main * optional parameters * @return string translation */ - public function translate(?string $lang, string $group, string $key, array $parameters = array()) : string + public static function translate(?string $lang, string $group, string $key, array $parameters = array()) : string { - if ($this->lang === null) { - $this->validLanguage($lang); - } - if ($this->lang !== null) { - if ($this->lang->getLang() !== $lang) { - $l = new Language($lang); - return $l->printlang($group, $key, $parameters); - } - return $this->lang->printlang($group, $key, $parameters); - } else { - return $key; - } + $l = new Language($lang); + return $l->printlang($group, $key, $parameters); } /** @@ -236,11 +262,10 @@ class Main * * @param ?string $lang */ - public function validLanguage(?string $lang) : bool + public static function validLanguage(?string $lang) : bool { $language = new Language($lang); if ($language->getLang() === $lang) { - $this->lang = $language; return true; } return false; diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php new file mode 100644 index 0000000..ca7a5cd --- /dev/null +++ b/plugins/federator/contentnation.php @@ -0,0 +1,83 @@ + $config + */ + public function __construct($config) + { + $this->service = $config['service-uri']; + } + + /** + * get remote user by given session + * + * @param string $_session session id + * @param string $_user user or profile name + * @return \Federator\Data\User | false + */ + public function getRemoteUserBySession(string $_session, string $_user) + { + // validate $_session and $user + if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) { + return false; + } + if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) { + return false; + } + $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user); + $headers = ['Cookie: session=' . $_session, 'Accept: application/json']; + [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); + + if ($info['http_code'] != 200) { + return false; + } + $r = json_decode($response, true); + if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) { + return false; + } + $user = new \Federator\Data\User(); + $user->externalid = $_user; + $user->permissions = []; + $user->session = $_session; + foreach ($r[$_user] as $p) { + $user->permissions[] = $p; + } + return $user; + } +} + +namespace Federator; + +/** + * Function to initialize plugin + * + * @param \Federator\Main $main main instance + * @return void + */ +function contentnation_load($main) +{ + $cn = new Connector\ContentNation($main->getConfig()['contentnation']); + $main->setConnector($cn); +} diff --git a/progress.md b/progress.md index 88ea82b..073221c 100644 --- a/progress.md +++ b/progress.md @@ -17,7 +17,7 @@ primary goal is to connect ContentNation via ActivityPub again. - [X] cache layer for users minmal version - [X] overlay to extend with needed info like private keys, urls, ... - [X] full cache for users -- [ ] webfinger +- [X] webfinger - [ ] discovery endpoints - [ ] ap outbox - [ ] ap inbox diff --git a/templates/federator/webfinger_acct.json b/templates/federator/webfinger_acct.json new file mode 100644 index 0000000..4777c44 --- /dev/null +++ b/templates/federator/webfinger_acct.json @@ -0,0 +1,11 @@ +{ldelim} + "subject": "acct:{$username}@{$domain}", + "aliases": ["https://{$domain}/@{$username}"], + "links": [ + {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim}, +{if $type=='Group'} + {ldelim}"rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://{$domain}/@{$username}/"{rdelim}, +{/if} + {ldelim}"rel": "http://ostatus.org/schema/1.0/subscribe", "template": "https://{$domain}/authorize_interaction?uri={ldelim}uri{rdelim}"{rdelim} + ] +{rdelim} \ No newline at end of file