initial webfinger support
This commit is contained in:
parent
eed9678dbf
commit
47efd74b6c
12 changed files with 364 additions and 59 deletions
|
@ -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]
|
||||
</Directory>
|
||||
|
||||
With the dummy plugin and everything installed correctly a
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -11,15 +11,17 @@ namespace Federator\Api;
|
|||
/**
|
||||
* API interface
|
||||
*/
|
||||
interface V1
|
||||
interface APIInterface
|
||||
{
|
||||
/**
|
||||
* run given url path
|
||||
*
|
||||
* @param array<string> $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
|
|
@ -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<string> $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':
|
||||
|
|
84
php/federator/api/wellknown.php
Normal file
84
php/federator/api/wellknown.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* @author Sascha Nitsch (grumpydeveloper)
|
||||
**/
|
||||
|
||||
namespace Federator\Api;
|
||||
|
||||
/**
|
||||
* .well-known handlers
|
||||
*/
|
||||
class WellKnown implements APIInterface
|
||||
{
|
||||
/**
|
||||
* main instance
|
||||
*
|
||||
* @var \Federator\Main $main
|
||||
*/
|
||||
private $main;
|
||||
|
||||
/**
|
||||
* response from sub-calls
|
||||
*
|
||||
* @var string $response
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param \Federator\Main $main main instance
|
||||
*/
|
||||
public function __construct($main)
|
||||
{
|
||||
$this->main = $main;
|
||||
}
|
||||
|
||||
/**
|
||||
* run given url path
|
||||
*
|
||||
* @param array<string> $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;
|
||||
}
|
||||
}
|
66
php/federator/api/wellknown/webfinger.php
Normal file
66
php/federator/api/wellknown/webfinger.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* @author Sascha Nitsch (grumpydeveloper)
|
||||
**/
|
||||
|
||||
namespace Federator\Api\WellKnown;
|
||||
|
||||
class WebFinger
|
||||
{
|
||||
/**
|
||||
* parent instance
|
||||
*
|
||||
* @var \Federator\Api\WellKnown $wellKnown
|
||||
*/
|
||||
private $wellKnown;
|
||||
|
||||
/**
|
||||
* main instance
|
||||
*
|
||||
* @var \Federator\Main $main
|
||||
*/
|
||||
private $main;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param \Federator\Api\WellKnown $wellKnown parent instance
|
||||
* @param \Federator\Main $main main instance
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($wellKnown, $main)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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<string,string> $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<string, mixed> $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;
|
||||
|
|
83
plugins/federator/contentnation.php
Normal file
83
plugins/federator/contentnation.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* @author Sascha Nitsch (grumpydeveloper)
|
||||
**/
|
||||
|
||||
namespace Federator\Connector;
|
||||
|
||||
/**
|
||||
* Connector to ContentNation.net
|
||||
*/
|
||||
class ContentNation implements Connector
|
||||
{
|
||||
/**
|
||||
* service-URL
|
||||
*
|
||||
* @var string $service
|
||||
*/
|
||||
private $service;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param array<string, mixed> $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);
|
||||
}
|
|
@ -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
|
||||
|
|
11
templates/federator/webfinger_acct.json
Normal file
11
templates/federator/webfinger_acct.json
Normal file
|
@ -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}
|
Loading…
Reference in a new issue