Merge pull request #5253 from rabuzarus/20180616_-_magic_auth_test_2
Port hubzillas OpenWebAuth - remote authentification
This commit is contained in:
commit
a5550b4702
15 changed files with 1196 additions and 50 deletions
2
boot.php
2
boot.php
|
@ -41,7 +41,7 @@ define('FRIENDICA_PLATFORM', 'Friendica');
|
||||||
define('FRIENDICA_CODENAME', 'The Tazmans Flax-lily');
|
define('FRIENDICA_CODENAME', 'The Tazmans Flax-lily');
|
||||||
define('FRIENDICA_VERSION', '2018.08-dev');
|
define('FRIENDICA_VERSION', '2018.08-dev');
|
||||||
define('DFRN_PROTOCOL_VERSION', '2.23');
|
define('DFRN_PROTOCOL_VERSION', '2.23');
|
||||||
define('DB_UPDATE_VERSION', 1268);
|
define('DB_UPDATE_VERSION', 1269);
|
||||||
define('NEW_UPDATE_ROUTINE_VERSION', 1170);
|
define('NEW_UPDATE_ROUTINE_VERSION', 1170);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
17
database.sql
17
database.sql
|
@ -1,6 +1,6 @@
|
||||||
-- ------------------------------------------
|
-- ------------------------------------------
|
||||||
-- Friendica 2018.08-dev (The Tazmans Flax-lily)
|
-- Friendica 2018.08-dev (The Tazmans Flax-lily)
|
||||||
-- DB_UPDATE_VERSION 1268
|
-- DB_UPDATE_VERSION 1269
|
||||||
-- ------------------------------------------
|
-- ------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@ -375,7 +375,7 @@ CREATE TABLE IF NOT EXISTS `group` (
|
||||||
CREATE TABLE IF NOT EXISTS `group_member` (
|
CREATE TABLE IF NOT EXISTS `group_member` (
|
||||||
`id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID',
|
`id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID',
|
||||||
`gid` int unsigned NOT NULL DEFAULT 0 COMMENT 'groups.id of the associated group',
|
`gid` int unsigned NOT NULL DEFAULT 0 COMMENT 'groups.id of the associated group',
|
||||||
`contact-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact.id of the member assigned to the associated group',
|
`contact-id` int unsigned NOT NULL DEFAULT 0 COMMENT 'contact.id of the member assigned to the associated group',
|
||||||
PRIMARY KEY(`id`),
|
PRIMARY KEY(`id`),
|
||||||
INDEX `contactid` (`contact-id`),
|
INDEX `contactid` (`contact-id`),
|
||||||
UNIQUE INDEX `gid_contactid` (`gid`,`contact-id`)
|
UNIQUE INDEX `gid_contactid` (`gid`,`contact-id`)
|
||||||
|
@ -1084,6 +1084,19 @@ CREATE TABLE IF NOT EXISTS `user-item` (
|
||||||
PRIMARY KEY(`uid`,`iid`)
|
PRIMARY KEY(`uid`,`iid`)
|
||||||
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User specific item data';
|
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='User specific item data';
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TABLE openwebauth-token
|
||||||
|
--
|
||||||
|
CREATE TABLE IF NOT EXISTS `openwebauth-token` (
|
||||||
|
`id` int(10) NOT NULL auto_increment COMMENT 'sequential ID',
|
||||||
|
`uid` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'User id',
|
||||||
|
`type` varchar(32) DEFAULT '' COMMENT 'Verify type',
|
||||||
|
`token` varchar(255) DEFAULT '' COMMENT 'A generated token',
|
||||||
|
`meta` varchar(255) DEFAULT '' COMMENT '',
|
||||||
|
`created` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'datetime of creation',
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Store OpenWebAuth token to verify contacts';
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TABLE worker-ipc
|
-- TABLE worker-ipc
|
||||||
--
|
--
|
||||||
|
|
|
@ -357,6 +357,12 @@ Hook data:
|
||||||
'item' => item array (input)
|
'item' => item array (input)
|
||||||
'html' => converted item body (input/output)
|
'html' => converted item body (input/output)
|
||||||
|
|
||||||
|
### 'magic_auth_success'
|
||||||
|
Called when a magic-auth was successful.
|
||||||
|
Hook data:
|
||||||
|
'visitor' => array with the contact record of the visitor
|
||||||
|
'url' => the query string
|
||||||
|
|
||||||
Current JavaScript hooks
|
Current JavaScript hooks
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -557,6 +563,7 @@ Here is a complete list of all hook callbacks with file locations (as of 01-Apr-
|
||||||
Addon::callHooks('profile_sidebar', $arr);
|
Addon::callHooks('profile_sidebar', $arr);
|
||||||
Addon::callHooks('profile_tabs', $arr);
|
Addon::callHooks('profile_tabs', $arr);
|
||||||
Addon::callHooks('zrl_init', $arr);
|
Addon::callHooks('zrl_init', $arr);
|
||||||
|
Addon::callHooks('magic_auth_success', $arr);
|
||||||
|
|
||||||
### src/Model/Event.php
|
### src/Model/Event.php
|
||||||
|
|
||||||
|
|
42
index.php
42
index.php
|
@ -121,25 +121,35 @@ if ((x($_SESSION, 'language')) && ($_SESSION['language'] !== $lang)) {
|
||||||
L10n::loadTranslationTable($lang);
|
L10n::loadTranslationTable($lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((x($_GET, 'zrl')) && $a->mode == App::MODE_NORMAL) {
|
if ((x($_GET,'zrl')) && $a->mode == App::MODE_NORMAL) {
|
||||||
// Only continue when the given profile link seems valid
|
$a->query_string = Profile::stripZrls($a->query_string);
|
||||||
// Valid profile links contain a path with "/profile/" and no query parameters
|
if (!local_user()) {
|
||||||
if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "")
|
// Only continue when the given profile link seems valid
|
||||||
&& strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")
|
// Valid profile links contain a path with "/profile/" and no query parameters
|
||||||
) {
|
if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
|
||||||
$_SESSION['my_url'] = $_GET['zrl'];
|
strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
|
||||||
$a->query_string = preg_replace('/[\?&]zrl=(.*?)([\?&]|$)/is', '', $a->query_string);
|
if ($_SESSION["visitor_home"] != $_GET["zrl"]) {
|
||||||
Profile::zrlInit($a);
|
$_SESSION['my_url'] = $_GET['zrl'];
|
||||||
} else {
|
$_SESSION['authenticated'] = 0;
|
||||||
// Someone came with an invalid parameter, maybe as a DDoS attempt
|
}
|
||||||
// We simply stop processing here
|
Profile::zrlInit($a);
|
||||||
logger("Invalid ZRL parameter ".$_GET['zrl'], LOGGER_DEBUG);
|
} else {
|
||||||
header('HTTP/1.1 403 Forbidden');
|
// Someone came with an invalid parameter, maybe as a DDoS attempt
|
||||||
echo "<h1>403 Forbidden</h1>";
|
// We simply stop processing here
|
||||||
killme();
|
logger("Invalid ZRL parameter " . $_GET['zrl'], LOGGER_DEBUG);
|
||||||
|
header('HTTP/1.1 403 Forbidden');
|
||||||
|
echo "<h1>403 Forbidden</h1>";
|
||||||
|
killme();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((x($_GET,'owt')) && $a->mode == App::MODE_NORMAL) {
|
||||||
|
$token = $_GET['owt'];
|
||||||
|
$a->query_string = Profile::stripQueryParam($a->query_string, 'owt');
|
||||||
|
Profile::openWebAuthInit($token);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For Mozilla auth manager - still needs sorting, and this might conflict with LRDD header.
|
* For Mozilla auth manager - still needs sorting, and this might conflict with LRDD header.
|
||||||
* Apache/PHP lumps the Link: headers into one - and other services might not be able to parse it
|
* Apache/PHP lumps the Link: headers into one - and other services might not be able to parse it
|
||||||
|
|
34
mod/xrd.php
34
mod/xrd.php
|
@ -66,20 +66,23 @@ function xrd_json($a, $uri, $alias, $profile_url, $r)
|
||||||
header("Content-type: application/json; charset=utf-8");
|
header("Content-type: application/json; charset=utf-8");
|
||||||
|
|
||||||
$json = ['subject' => $uri,
|
$json = ['subject' => $uri,
|
||||||
'aliases' => [$alias, $profile_url],
|
'aliases' => [$alias, $profile_url],
|
||||||
'links' => [['rel' => NAMESPACE_DFRN, 'href' => $profile_url],
|
'links' => [
|
||||||
['rel' => NAMESPACE_FEED, 'type' => 'application/atom+xml', 'href' => System::baseUrl().'/dfrn_poll/'.$r['nickname']],
|
['rel' => NAMESPACE_DFRN, 'href' => $profile_url],
|
||||||
['rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', 'href' => $profile_url],
|
['rel' => NAMESPACE_FEED, 'type' => 'application/atom+xml', 'href' => System::baseUrl().'/dfrn_poll/'.$r['nickname']],
|
||||||
['rel' => 'http://microformats.org/profile/hcard', 'type' => 'text/html', 'href' => System::baseUrl().'/hcard/'.$r['nickname']],
|
['rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', 'href' => $profile_url],
|
||||||
['rel' => NAMESPACE_POCO, 'href' => System::baseUrl().'/poco/'.$r['nickname']],
|
['rel' => 'http://microformats.org/profile/hcard', 'type' => 'text/html', 'href' => System::baseUrl().'/hcard/'.$r['nickname']],
|
||||||
['rel' => 'http://webfinger.net/rel/avatar', 'type' => 'image/jpeg', 'href' => System::baseUrl().'/photo/profile/'.$r['uid'].'.jpg'],
|
['rel' => NAMESPACE_POCO, 'href' => System::baseUrl().'/poco/'.$r['nickname']],
|
||||||
['rel' => 'http://joindiaspora.com/seed_location', 'type' => 'text/html', 'href' => System::baseUrl()],
|
['rel' => 'http://webfinger.net/rel/avatar', 'type' => 'image/jpeg', 'href' => System::baseUrl().'/photo/profile/'.$r['uid'].'.jpg'],
|
||||||
['rel' => 'salmon', 'href' => System::baseUrl().'/salmon/'.$r['nickname']],
|
['rel' => 'http://joindiaspora.com/seed_location', 'type' => 'text/html', 'href' => System::baseUrl()],
|
||||||
['rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => System::baseUrl().'/salmon/'.$r['nickname']],
|
['rel' => 'salmon', 'href' => System::baseUrl().'/salmon/'.$r['nickname']],
|
||||||
['rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => System::baseUrl().'/salmon/'.$r['nickname'].'/mention'],
|
['rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => System::baseUrl().'/salmon/'.$r['nickname']],
|
||||||
['rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => System::baseUrl().'/follow?url={uri}'],
|
['rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => System::baseUrl().'/salmon/'.$r['nickname'].'/mention'],
|
||||||
['rel' => 'magic-public-key', 'href' => 'data:application/magic-public-key,'.$salmon_key]
|
['rel' => 'http://ostatus.org/schema/1.0/subscribe', 'template' => System::baseUrl().'/follow?url={uri}'],
|
||||||
]];
|
['rel' => 'magic-public-key', 'href' => 'data:application/magic-public-key,'.$salmon_key],
|
||||||
|
['rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-dfrn+json', 'href' => System::baseUrl().'/owa']
|
||||||
|
]
|
||||||
|
];
|
||||||
echo json_encode($json);
|
echo json_encode($json);
|
||||||
killme();
|
killme();
|
||||||
}
|
}
|
||||||
|
@ -102,10 +105,11 @@ function xrd_xml($a, $uri, $alias, $profile_url, $r)
|
||||||
'$atom' => System::baseUrl() . '/dfrn_poll/' . $r['nickname'],
|
'$atom' => System::baseUrl() . '/dfrn_poll/' . $r['nickname'],
|
||||||
'$poco_url' => System::baseUrl() . '/poco/' . $r['nickname'],
|
'$poco_url' => System::baseUrl() . '/poco/' . $r['nickname'],
|
||||||
'$photo' => System::baseUrl() . '/photo/profile/' . $r['uid'] . '.jpg',
|
'$photo' => System::baseUrl() . '/photo/profile/' . $r['uid'] . '.jpg',
|
||||||
'$baseurl' => System::baseUrl(),
|
'$baseurl' => System::baseUrl(),
|
||||||
'$salmon' => System::baseUrl() . '/salmon/' . $r['nickname'],
|
'$salmon' => System::baseUrl() . '/salmon/' . $r['nickname'],
|
||||||
'$salmen' => System::baseUrl() . '/salmon/' . $r['nickname'] . '/mention',
|
'$salmen' => System::baseUrl() . '/salmon/' . $r['nickname'] . '/mention',
|
||||||
'$subscribe' => System::baseUrl() . '/follow?url={uri}',
|
'$subscribe' => System::baseUrl() . '/follow?url={uri}',
|
||||||
|
'$openwebauth' => System::baseUrl() . '/owa',
|
||||||
'$modexp' => 'data:application/magic-public-key,' . $salmon_key]
|
'$modexp' => 'data:application/magic-public-key,' . $salmon_key]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -163,17 +163,17 @@ EOT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Encodes content to json
|
* @brief Encodes content to json.
|
||||||
*
|
*
|
||||||
* This function encodes an array to json format
|
* This function encodes an array to json format
|
||||||
* and adds an application/json HTTP header to the output.
|
* and adds an application/json HTTP header to the output.
|
||||||
* After finishing the process is getting killed.
|
* After finishing the process is getting killed.
|
||||||
*
|
*
|
||||||
* @param array $x The input content
|
* @param array $x The input content.
|
||||||
|
* @param string $content_type Type of the input (Default: 'application/json').
|
||||||
*/
|
*/
|
||||||
public static function jsonExit($x)
|
public static function jsonExit($x, $content_type = 'application/json') {
|
||||||
{
|
header("Content-type: $content_type");
|
||||||
header("content-type: application/json");
|
|
||||||
echo json_encode($x);
|
echo json_encode($x);
|
||||||
killme();
|
killme();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1818,6 +1818,20 @@ class DBStructure
|
||||||
"PRIMARY" => ["uid", "iid"],
|
"PRIMARY" => ["uid", "iid"],
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
$database["openwebauth-token"] = [
|
||||||
|
"comment" => "Store OpenWebAuth token to verify contacts",
|
||||||
|
"fields" => [
|
||||||
|
"id" => ["type" => "int(10)", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"],
|
||||||
|
"uid" => ["type" => "int(10) unsigned", "not null" => "1", "default" => "0", "relation" => ["user" => "uid"], "comment" => "User id"],
|
||||||
|
"type" => ["type" => "varchar(32)", "not_null", "default" => "", "comment" => "Verify type"],
|
||||||
|
"token" => ["type" => "varchar(255)", "not_null" => "1", "default" => "", "comment" => "A generated token"],
|
||||||
|
"meta" => ["type" => "varchar(255)", "not_null" => "1", "default" => "", "comment" => ""],
|
||||||
|
"created" => ["type" => "datetime", "not null" => "1", "default" => NULL_DATE, "comment" => "datetime of creation"],
|
||||||
|
],
|
||||||
|
"indexes" => [
|
||||||
|
"PRIMARY" => ["id"],
|
||||||
|
]
|
||||||
|
];
|
||||||
$database["worker-ipc"] = [
|
$database["worker-ipc"] = [
|
||||||
"comment" => "Inter process communication between the frontend and the worker",
|
"comment" => "Inter process communication between the frontend and the worker",
|
||||||
"fields" => [
|
"fields" => [
|
||||||
|
|
73
src/Model/OpenWebAuthToken.php
Normal file
73
src/Model/OpenWebAuthToken.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file src/Model/OpenWebAuthToken.php
|
||||||
|
*/
|
||||||
|
namespace Friendica\Model;
|
||||||
|
|
||||||
|
use Friendica\Database\DBM;
|
||||||
|
use Friendica\Util\DateTimeFormat;
|
||||||
|
use dba;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods to deal with entries of the 'openwebauth-token' table.
|
||||||
|
*/
|
||||||
|
class OpenWebAuthToken
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an entry in the 'openwebauth-token' table.
|
||||||
|
*
|
||||||
|
* @param string $type Verify type.
|
||||||
|
* @param int $uid The user ID.
|
||||||
|
* @param string $token
|
||||||
|
* @param string $meta
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public static function create($type, $uid, $token, $meta)
|
||||||
|
{
|
||||||
|
$fields = [
|
||||||
|
"type" => $type,
|
||||||
|
"uid" => $uid,
|
||||||
|
"token" => $token,
|
||||||
|
"meta" => $meta,
|
||||||
|
"created" => DateTimeFormat::utcNow()
|
||||||
|
];
|
||||||
|
return dba::insert("openwebauth-token", $fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the "meta" field of an entry in the openwebauth-token table.
|
||||||
|
*
|
||||||
|
* @param string $type Verify type.
|
||||||
|
* @param int $uid The user ID.
|
||||||
|
* @param string $token
|
||||||
|
*
|
||||||
|
* @return string|boolean The meta enry or false if not found.
|
||||||
|
*/
|
||||||
|
public static function getMeta($type, $uid, $token)
|
||||||
|
{
|
||||||
|
$condition = ["type" => $type, "uid" => $uid, "token" => $token];
|
||||||
|
|
||||||
|
$entry = dba::selectFirst("openwebauth-token", ["id", "meta"], $condition);
|
||||||
|
if (DBM::is_result($entry)) {
|
||||||
|
dba::delete("openwebauth-token", ["id" => $entry["id"]]);
|
||||||
|
|
||||||
|
return $entry["meta"];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge entries of a verify-type older than interval.
|
||||||
|
*
|
||||||
|
* @param string $type Verify type.
|
||||||
|
* @param string $interval SQL compatible time interval
|
||||||
|
*/
|
||||||
|
public static function purge($type, $interval)
|
||||||
|
{
|
||||||
|
$condition = ["`type` = ? AND `created` < ?", $type, DateTimeFormat::utcNow() . " - INTERVAL " . $interval];
|
||||||
|
dba::delete("openwebauth-token", $condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ use Friendica\Core\System;
|
||||||
use Friendica\Core\Worker;
|
use Friendica\Core\Worker;
|
||||||
use Friendica\Database\DBM;
|
use Friendica\Database\DBM;
|
||||||
use Friendica\Model\Contact;
|
use Friendica\Model\Contact;
|
||||||
|
use Friendica\Model\OpenWebAuthToken;
|
||||||
use Friendica\Protocol\Diaspora;
|
use Friendica\Protocol\Diaspora;
|
||||||
use Friendica\Util\DateTimeFormat;
|
use Friendica\Util\DateTimeFormat;
|
||||||
use Friendica\Util\Network;
|
use Friendica\Util\Network;
|
||||||
|
@ -978,27 +979,128 @@ class Profile
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the 'zrl' parameter and initiate the remote authentication.
|
||||||
|
*
|
||||||
|
* This method checks if the visitor has a public contact entry and
|
||||||
|
* redirects the visitor to his/her instance to start the magic auth (Authentication)
|
||||||
|
* process.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/channel.php
|
||||||
|
*
|
||||||
|
* @param App $a Application instance.
|
||||||
|
*/
|
||||||
public static function zrlInit(App $a)
|
public static function zrlInit(App $a)
|
||||||
{
|
{
|
||||||
$my_url = self::getMyURL();
|
$my_url = self::getMyURL();
|
||||||
$my_url = Network::isUrlValid($my_url);
|
$my_url = Network::isUrlValid($my_url);
|
||||||
|
|
||||||
if ($my_url) {
|
if ($my_url) {
|
||||||
// Is it a DDoS attempt?
|
if (!local_user()) {
|
||||||
// The check fetches the cached value from gprobe to reduce the load for this system
|
// Is it a DDoS attempt?
|
||||||
$urlparts = parse_url($my_url);
|
// The check fetches the cached value from gprobe to reduce the load for this system
|
||||||
|
$urlparts = parse_url($my_url);
|
||||||
|
|
||||||
$result = Cache::get('gprobe:' . $urlparts['host']);
|
$result = Cache::get('gprobe:' . $urlparts['host']);
|
||||||
if ((!is_null($result)) && (in_array($result['network'], [NETWORK_FEED, NETWORK_PHANTOM]))) {
|
if ((!is_null($result)) && (in_array($result['network'], [NETWORK_FEED, NETWORK_PHANTOM]))) {
|
||||||
logger('DDoS attempt detected for ' . $urlparts['host'] . ' by ' . $_SERVER['REMOTE_ADDR'] . '. server data: ' . print_r($_SERVER, true), LOGGER_DEBUG);
|
logger('DDoS attempt detected for ' . $urlparts['host'] . ' by ' . $_SERVER['REMOTE_ADDR'] . '. server data: ' . print_r($_SERVER, true), LOGGER_DEBUG);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Worker::add(PRIORITY_LOW, 'GProbe', $my_url);
|
||||||
|
$arr = ['zrl' => $my_url, 'url' => $a->cmd];
|
||||||
|
Addon::callHooks('zrl_init', $arr);
|
||||||
|
|
||||||
|
// Try to find the public contact entry of the visitor.
|
||||||
|
$cid = Contact::getIdForURL($my_url);
|
||||||
|
if (!$cid) {
|
||||||
|
logger('No contact record found for ' . $my_url, LOGGER_DEBUG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contact = dba::selectFirst('contact',['id', 'url'], ['id' => $cid]);
|
||||||
|
|
||||||
|
if (DBM::is_result($contact) && remote_user() && remote_user() === $contact['id']) {
|
||||||
|
// The visitor is already authenticated.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('Not authenticated. Invoking reverse magic-auth for ' . $my_url, LOGGER_DEBUG);
|
||||||
|
|
||||||
|
// Try to avoid recursion - but send them home to do a proper magic auth.
|
||||||
|
$query = str_replace(array('?zrl=', '&zid='), array('?rzrl=', '&rzrl='), $a->query_string);
|
||||||
|
// The other instance needs to know where to redirect.
|
||||||
|
$dest = urlencode(System::baseUrl() . '/' . $query);
|
||||||
|
|
||||||
|
// We need to extract the basebath from the profile url
|
||||||
|
// to redirect the visitors '/magic' module.
|
||||||
|
// Note: We should have the basepath of a contact also in the contact table.
|
||||||
|
$urlarr = explode('/profile/', $contact['url']);
|
||||||
|
$basepath = $urlarr[0];
|
||||||
|
|
||||||
|
if ($basepath != System::baseUrl() && !strstr($dest, '/magic') && !strstr($dest, '/rmagic')) {
|
||||||
|
goaway($basepath . '/magic' . '?f=&owa=1&dest=' . $dest);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Worker::add(PRIORITY_LOW, 'GProbe', $my_url);
|
|
||||||
$arr = ['zrl' => $my_url, 'url' => $a->cmd];
|
|
||||||
Addon::callHooks('zrl_init', $arr);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenWebAuth authentication.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/zid.php
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
*/
|
||||||
|
public static function openWebAuthInit($token)
|
||||||
|
{
|
||||||
|
$a = get_app();
|
||||||
|
|
||||||
|
// Clean old OpenWebAuthToken entries.
|
||||||
|
OpenWebAuthToken::purge('owt', '3 MINUTE');
|
||||||
|
|
||||||
|
// Check if the token we got is the same one
|
||||||
|
// we have stored in the database.
|
||||||
|
$visitor_handle = OpenWebAuthToken::getMeta('owt', 0, $token);
|
||||||
|
|
||||||
|
if($visitor_handle === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the public contact entry of the visitor.
|
||||||
|
$cid = Contact::getIdForURL($visitor_handle);
|
||||||
|
if(!$cid) {
|
||||||
|
logger('owt: unable to finger ' . $visitor_handle, LOGGER_DEBUG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$visitor = dba::selectFirst('contact', [], ['id' => $cid]);
|
||||||
|
|
||||||
|
// Authenticate the visitor.
|
||||||
|
$_SESSION['authenticated'] = 1;
|
||||||
|
$_SESSION['visitor_id'] = $visitor['id'];
|
||||||
|
$_SESSION['visitor_handle'] = $visitor['addr'];
|
||||||
|
$_SESSION['visitor_home'] = $visitor['url'];
|
||||||
|
|
||||||
|
$arr = [
|
||||||
|
'visitor' => $visitor,
|
||||||
|
'url' => $a->query_string
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* @hooks magic_auth_success
|
||||||
|
* Called when a magic-auth was successful.
|
||||||
|
* * \e array \b visitor
|
||||||
|
* * \e string \b url
|
||||||
|
*/
|
||||||
|
Addon::callHooks('magic_auth_success', $arr);
|
||||||
|
|
||||||
|
$a->contact = $arr['visitor'];
|
||||||
|
|
||||||
|
info(L10n::t('OpenWebAuth: %1$s welcomes %2$s', $a->get_hostname(), $visitor['name']));
|
||||||
|
|
||||||
|
logger('OpenWebAuth: auth success from ' . $visitor['addr'], LOGGER_DEBUG);
|
||||||
|
}
|
||||||
|
|
||||||
public static function zrl($s, $force = false)
|
public static function zrl($s, $force = false)
|
||||||
{
|
{
|
||||||
if (!strlen($s)) {
|
if (!strlen($s)) {
|
||||||
|
@ -1042,4 +1144,26 @@ class Profile
|
||||||
|
|
||||||
return $uid;
|
return $uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stip zrl parameter from a string.
|
||||||
|
*
|
||||||
|
* @param string $s The input string.
|
||||||
|
* @return string The zrl.
|
||||||
|
*/
|
||||||
|
public static function stripZrls($s)
|
||||||
|
{
|
||||||
|
return preg_replace('/[\?&]zrl=(.*?)([\?&]|$)/is', '', $s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stip query parameter from a string.
|
||||||
|
*
|
||||||
|
* @param string $s The input string.
|
||||||
|
* @return string The query parameter.
|
||||||
|
*/
|
||||||
|
public static function stripQueryParam($s, $param)
|
||||||
|
{
|
||||||
|
return preg_replace('/[\?&]' . $param . '=(.*?)(&|$)/ism', '$2', $s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
121
src/Module/Magic.php
Normal file
121
src/Module/Magic.php
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @file src/Module/Magic.php
|
||||||
|
*/
|
||||||
|
namespace Friendica\Module;
|
||||||
|
|
||||||
|
use Friendica\BaseModule;
|
||||||
|
use Friendica\Model\Contact;
|
||||||
|
use Friendica\Util\HTTPSignature;
|
||||||
|
use Friendica\Util\Network;
|
||||||
|
|
||||||
|
use dba;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magic Auth (remote authentication) module.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/Zotlabs/Module/Magic.php
|
||||||
|
*/
|
||||||
|
class Magic extends BaseModule
|
||||||
|
{
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
|
$a = self::getApp();
|
||||||
|
$ret = ['success' => false, 'url' => '', 'message' => ''];
|
||||||
|
logger('magic mdule: invoked', LOGGER_DEBUG);
|
||||||
|
|
||||||
|
logger('args: ' . print_r($_REQUEST, true), LOGGER_DATA);
|
||||||
|
|
||||||
|
$addr = ((x($_REQUEST, 'addr')) ? $_REQUEST['addr'] : '');
|
||||||
|
$dest = ((x($_REQUEST, 'dest')) ? $_REQUEST['dest'] : '');
|
||||||
|
$test = ((x($_REQUEST, 'test')) ? intval($_REQUEST['test']) : 0);
|
||||||
|
$owa = ((x($_REQUEST, 'owa')) ? intval($_REQUEST['owa']) : 0);
|
||||||
|
|
||||||
|
// NOTE: I guess $dest isn't just the profile url (could be also
|
||||||
|
// other profile pages e.g. photo). We need to find a solution
|
||||||
|
// to be able to redirct to other pages than the contact profile.
|
||||||
|
$cid = Contact::getIdForURL($dest);
|
||||||
|
|
||||||
|
if (!$cid && !empty($addr)) {
|
||||||
|
$cid = Contact::getIdForURL($addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$cid) {
|
||||||
|
logger('No contact record found: ' . print_r($_REQUEST, true), LOGGER_DEBUG);
|
||||||
|
goaway($dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contact = dba::selectFirst('contact', ['id', 'nurl', 'url'], ['id' => $cid]);
|
||||||
|
|
||||||
|
// Redirect if the contact is already authenticated on this site.
|
||||||
|
if (array_key_exists('id', $a->contact) && strpos($contact['nurl'], normalise_link(self::getApp()->get_baseurl())) !== false) {
|
||||||
|
if($test) {
|
||||||
|
$ret['success'] = true;
|
||||||
|
$ret['message'] .= 'Local site - you are already authenticated.' . EOL;
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('Contact is already authenticated', LOGGER_DEBUG);
|
||||||
|
goaway($dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local_user()) {
|
||||||
|
$user = $a->user;
|
||||||
|
|
||||||
|
// OpenWebAuth
|
||||||
|
if ($owa) {
|
||||||
|
// Extract the basepath
|
||||||
|
// NOTE: we need another solution because this does only work
|
||||||
|
// for friendica contacts :-/ . We should have the basepath
|
||||||
|
// of a contact also in the contact table.
|
||||||
|
$exp = explode('/profile/', $contact['url']);
|
||||||
|
$basepath = $exp[0];
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
$headers['Accept'] = 'application/x-dfrn+json';
|
||||||
|
$headers['X-Open-Web-Auth'] = random_string();
|
||||||
|
|
||||||
|
// Create a header that is signed with the local users private key.
|
||||||
|
$headers = HTTPSignature::createSig(
|
||||||
|
'',
|
||||||
|
$headers,
|
||||||
|
$user['prvkey'],
|
||||||
|
'acct:' . $user['nickname'] . '@' . $a->get_hostname() . ($a->path ? '/' . $a->path : ''),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
'sha512'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to get an authentication token from the other instance.
|
||||||
|
$x = Network::curl($basepath . '/owa', false, $redirects, ['headers' => $headers]);
|
||||||
|
|
||||||
|
if ($x['success']) {
|
||||||
|
$j = json_decode($x['body'], true);
|
||||||
|
|
||||||
|
if ($j['success']) {
|
||||||
|
$token = '';
|
||||||
|
if ($j['encrypted_token']) {
|
||||||
|
// The token is encrypted. If the local user is really the one the other instance
|
||||||
|
// thinks he/she is, the token can be decrypted with the local users public key.
|
||||||
|
openssl_private_decrypt(base64url_decode($j['encrypted_token']), $token, $user['prvkey']);
|
||||||
|
} else {
|
||||||
|
$token = $j['token'];
|
||||||
|
}
|
||||||
|
$x = strpbrk($dest, '?&');
|
||||||
|
$args = (($x) ? '&owt=' . $token : '?f=&owt=' . $token);
|
||||||
|
|
||||||
|
goaway($dest . $args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goaway($dest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($test) {
|
||||||
|
$ret['message'] = 'Not authenticated or invalid arguments' . EOL;
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
goaway($dest);
|
||||||
|
}
|
||||||
|
}
|
91
src/Module/Owa.php
Normal file
91
src/Module/Owa.php
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @file src/Module/Owa.php
|
||||||
|
*/
|
||||||
|
namespace Friendica\Module;
|
||||||
|
|
||||||
|
use Friendica\BaseModule;
|
||||||
|
use Friendica\Core\System;
|
||||||
|
use Friendica\Database\DBM;
|
||||||
|
use Friendica\Model\Contact;
|
||||||
|
use Friendica\Model\OpenWebAuthToken;
|
||||||
|
use Friendica\Util\HTTPSignature;
|
||||||
|
|
||||||
|
use dba;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief OpenWebAuth verifier and token generator
|
||||||
|
*
|
||||||
|
* See https://macgirvin.com/wiki/mike/OpenWebAuth/Home
|
||||||
|
* Requests to this endpoint should be signed using HTTP Signatures
|
||||||
|
* using the 'Authorization: Signature' authentication method
|
||||||
|
* If the signature verifies a token is returned.
|
||||||
|
*
|
||||||
|
* This token may be exchanged for an authenticated cookie.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/Zotlabs/Module/Owa.php
|
||||||
|
*/
|
||||||
|
class Owa extends BaseModule
|
||||||
|
{
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
|
|
||||||
|
$ret = [ 'success' => false ];
|
||||||
|
|
||||||
|
foreach (['REDIRECT_REMOTE_USER', 'HTTP_AUTHORIZATION'] as $head) {
|
||||||
|
if (array_key_exists($head, $_SERVER) && substr(trim($_SERVER[$head]), 0, 9) === 'Signature') {
|
||||||
|
if ($head !== 'HTTP_AUTHORIZATION') {
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = $_SERVER[$head];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sigblock = HTTPSignature::parseSigheader($_SERVER[$head]);
|
||||||
|
if ($sigblock) {
|
||||||
|
$keyId = $sigblock['keyId'];
|
||||||
|
|
||||||
|
if ($keyId) {
|
||||||
|
// Try to find the public contact entry of the handle.
|
||||||
|
$handle = str_replace('acct:', '', $keyId);
|
||||||
|
|
||||||
|
$cid = Contact::getIdForURL($handle);
|
||||||
|
$fields = ['id', 'url', 'addr', 'pubkey'];
|
||||||
|
$condition = ['id' => $cid];
|
||||||
|
|
||||||
|
$contact = dba::selectFirst('contact', $fields, $condition);
|
||||||
|
|
||||||
|
if (DBM::is_result($contact)) {
|
||||||
|
// Try to verify the signed header with the public key of the contact record
|
||||||
|
// we have found.
|
||||||
|
$verified = HTTPSignature::verify('', $contact['pubkey']);
|
||||||
|
|
||||||
|
if ($verified && $verified['header_signed'] && $verified['header_valid']) {
|
||||||
|
logger('OWA header: ' . print_r($verified, true), LOGGER_DATA);
|
||||||
|
logger('OWA success: ' . $contact['addr'], LOGGER_DATA);
|
||||||
|
|
||||||
|
$ret['success'] = true;
|
||||||
|
$token = random_string(32);
|
||||||
|
|
||||||
|
// Store the generated token in the databe.
|
||||||
|
OpenWebAuthToken::create('owt', 0, $token, $contact['addr']);
|
||||||
|
|
||||||
|
$result = '';
|
||||||
|
|
||||||
|
// Encrypt the token with the public contacts publik key.
|
||||||
|
// Only the specific public contact will be able to encrypt it.
|
||||||
|
// At a later time, we will compare weather the token we're getting
|
||||||
|
// is really the same token we have stored in the database.
|
||||||
|
openssl_public_encrypt($token, $result, $contact['pubkey']);
|
||||||
|
$ret['encrypted_token'] = base64url_encode($result);
|
||||||
|
} else {
|
||||||
|
logger('OWA fail: ' . $contact['id'] . ' ' . $contact['addr'] . ' ' . $contact['url'], LOGGER_DEBUG);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger('Contact not found: ' . $handle, LOGGER_DEBUG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System::jsonExit($ret, 'application/x-dfrn+json');
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
namespace Friendica\Util;
|
namespace Friendica\Util;
|
||||||
|
|
||||||
|
use Friendica\Core\Addon;
|
||||||
use Friendica\Core\Config;
|
use Friendica\Core\Config;
|
||||||
use ASN_BASE;
|
use ASN_BASE;
|
||||||
use ASNValue;
|
use ASNValue;
|
||||||
|
@ -246,4 +247,232 @@ class Crypto
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt a string with 'aes-256-cbc' cipher method.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $key The key used for encryption.
|
||||||
|
* @param string $iv A non-NULL Initialization Vector.
|
||||||
|
*
|
||||||
|
* @return string|boolean Encrypted string or false on failure.
|
||||||
|
*/
|
||||||
|
private static function encryptAES256CBC($data, $key, $iv)
|
||||||
|
{
|
||||||
|
return openssl_encrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a string with 'aes-256-cbc' cipher method.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $key The key used for decryption.
|
||||||
|
* @param string $iv A non-NULL Initialization Vector.
|
||||||
|
*
|
||||||
|
* @return string|boolean Decrypted string or false on failure.
|
||||||
|
*/
|
||||||
|
private static function decryptAES256CBC($data, $key, $iv)
|
||||||
|
{
|
||||||
|
return openssl_decrypt($data, 'aes-256-cbc', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt a string with 'aes-256-ctr' cipher method.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $key The key used for encryption.
|
||||||
|
* @param string $iv A non-NULL Initialization Vector.
|
||||||
|
*
|
||||||
|
* @return string|boolean Encrypted string or false on failure.
|
||||||
|
*/
|
||||||
|
private static function encryptAES256CTR($data, $key, $iv)
|
||||||
|
{
|
||||||
|
$key = substr($key, 0, 32);
|
||||||
|
$iv = substr($iv, 0, 16);
|
||||||
|
return openssl_encrypt($data, 'aes-256-ctr', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a string with 'aes-256-ctr' cipher method.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $key The key used for decryption.
|
||||||
|
* @param string $iv A non-NULL Initialization Vector.
|
||||||
|
*
|
||||||
|
* @return string|boolean Decrypted string or false on failure.
|
||||||
|
*/
|
||||||
|
private static function decryptAES256CTR($data, $key, $iv)
|
||||||
|
{
|
||||||
|
$key = substr($key, 0, 32);
|
||||||
|
$iv = substr($iv, 0, 16);
|
||||||
|
return openssl_decrypt($data, 'aes-256-ctr', str_pad($key, 32, "\0"), OPENSSL_RAW_DATA, str_pad($iv, 16, "\0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $pubkey The public key.
|
||||||
|
* @param string $alg The algorithm used for encryption.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function encapsulate($data, $pubkey, $alg = 'aes256cbc')
|
||||||
|
{
|
||||||
|
if ($alg === 'aes256cbc') {
|
||||||
|
return self::encapsulateAes($data, $pubkey);
|
||||||
|
}
|
||||||
|
return self::encapsulateOther($data, $pubkey, $alg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param type $data
|
||||||
|
* @param type $pubkey The public key.
|
||||||
|
* @param type $alg The algorithm used for encryption.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function encapsulateOther($data, $pubkey, $alg)
|
||||||
|
{
|
||||||
|
if (!$pubkey) {
|
||||||
|
logger('no key. data: '.$data);
|
||||||
|
}
|
||||||
|
$fn = 'encrypt' . strtoupper($alg);
|
||||||
|
if (method_exists(__CLASS__, $fn)) {
|
||||||
|
$result = ['encrypted' => true];
|
||||||
|
$key = random_bytes(256);
|
||||||
|
$iv = random_bytes(256);
|
||||||
|
$result['data'] = base64url_encode(self::$fn($data, $key, $iv), true);
|
||||||
|
|
||||||
|
// log the offending call so we can track it down
|
||||||
|
if (!openssl_public_encrypt($key, $k, $pubkey)) {
|
||||||
|
$x = debug_backtrace();
|
||||||
|
logger('RSA failed. ' . print_r($x[0], true));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['alg'] = $alg;
|
||||||
|
$result['key'] = base64url_encode($k, true);
|
||||||
|
openssl_public_encrypt($iv, $i, $pubkey);
|
||||||
|
$result['iv'] = base64url_encode($i, true);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
} else {
|
||||||
|
$x = ['data' => $data, 'pubkey' => $pubkey, 'alg' => $alg, 'result' => $data];
|
||||||
|
Addon::callHooks('other_encapsulate', $x);
|
||||||
|
|
||||||
|
return $x['result'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $pubkey
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function encapsulateAes($data, $pubkey)
|
||||||
|
{
|
||||||
|
if (!$pubkey) {
|
||||||
|
logger('aes_encapsulate: no key. data: ' . $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = random_bytes(32);
|
||||||
|
$iv = random_bytes(16);
|
||||||
|
$result = ['encrypted' => true];
|
||||||
|
$result['data'] = base64url_encode(self::encryptAES256CBC($data, $key, $iv), true);
|
||||||
|
|
||||||
|
// log the offending call so we can track it down
|
||||||
|
if (!openssl_public_encrypt($key, $k, $pubkey)) {
|
||||||
|
$x = debug_backtrace();
|
||||||
|
logger('aes_encapsulate: RSA failed. ' . print_r($x[0], true));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['alg'] = 'aes256cbc';
|
||||||
|
$result['key'] = base64url_encode($k, true);
|
||||||
|
openssl_public_encrypt($iv, $i, $pubkey);
|
||||||
|
$result['iv'] = base64url_encode($i, true);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $prvkey The private key used for decryption.
|
||||||
|
*
|
||||||
|
* @return string|boolean The decrypted string or false on failure.
|
||||||
|
*/
|
||||||
|
public static function unencapsulate($data, $prvkey)
|
||||||
|
{
|
||||||
|
if (!$data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$alg = ((array_key_exists('alg', $data)) ? $data['alg'] : 'aes256cbc');
|
||||||
|
if ($alg === 'aes256cbc') {
|
||||||
|
return self::encapsulateAes($data, $prvkey);
|
||||||
|
}
|
||||||
|
return self::encapsulateOther($data, $prvkey, $alg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
* @param string $prvkey The private key used for decryption.
|
||||||
|
* @param string $alg
|
||||||
|
*
|
||||||
|
* @return string|boolean The decrypted string or false on failure.
|
||||||
|
*/
|
||||||
|
private static function unencapsulateOther($data, $prvkey, $alg)
|
||||||
|
{
|
||||||
|
$fn = 'decrypt' . strtoupper($alg);
|
||||||
|
|
||||||
|
if (method_exists(__CLASS__, $fn)) {
|
||||||
|
openssl_private_decrypt(base64url_decode($data['key']), $k, $prvkey);
|
||||||
|
openssl_private_decrypt(base64url_decode($data['iv']), $i, $prvkey);
|
||||||
|
|
||||||
|
return self::$fn(base64url_decode($data['data']), $k, $i);
|
||||||
|
} else {
|
||||||
|
$x = ['data' => $data, 'prvkey' => $prvkey, 'alg' => $alg, 'result' => $data];
|
||||||
|
Addon::callHooks('other_unencapsulate', $x);
|
||||||
|
|
||||||
|
return $x['result'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/include/crypto.php
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @param string $prvkey The private key used for decryption.
|
||||||
|
*
|
||||||
|
* @return string|boolean The decrypted string or false on failure.
|
||||||
|
*/
|
||||||
|
private static function unencapsulateAes($data, $prvkey)
|
||||||
|
{
|
||||||
|
openssl_private_decrypt(base64url_decode($data['key']), $k, $prvkey);
|
||||||
|
openssl_private_decrypt(base64url_decode($data['iv']), $i, $prvkey);
|
||||||
|
|
||||||
|
return self::decryptAES256CBC(base64url_decode($data['data']), $k, $i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
48
src/Util/HTTPHeaders.php
Normal file
48
src/Util/HTTPHeaders.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @file src/Util/HTTPHeaders.php
|
||||||
|
*/
|
||||||
|
namespace Friendica\Util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/Zotlabs/Web/HTTPHeaders.php
|
||||||
|
*/
|
||||||
|
class HTTPHeaders
|
||||||
|
{
|
||||||
|
private $in_progress = [];
|
||||||
|
private $parsed = [];
|
||||||
|
|
||||||
|
function __construct($headers)
|
||||||
|
{
|
||||||
|
$lines = explode("\n", str_replace("\r", '', $headers));
|
||||||
|
|
||||||
|
if ($lines) {
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (preg_match('/^\s+/', $line, $matches) && trim($line)) {
|
||||||
|
if (!empty($this->in_progress['k'])) {
|
||||||
|
$this->in_progress['v'] .= ' ' . ltrim($line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!empty($this->in_progress['k'])) {
|
||||||
|
$this->parsed[] = [$this->in_progress['k'] => $this->in_progress['v']];
|
||||||
|
$this->in_progress = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->in_progress['k'] = strtolower(substr($line, 0, strpos($line, ':')));
|
||||||
|
$this->in_progress['v'] = ltrim(substr($line, strpos($line, ':') + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->in_progress['k'])) {
|
||||||
|
$this->parsed[$this->in_progress['k']] = $this->in_progress['v'];
|
||||||
|
$this->in_progress = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetch()
|
||||||
|
{
|
||||||
|
return $this->parsed;
|
||||||
|
}
|
||||||
|
}
|
409
src/Util/HTTPSignature.php
Normal file
409
src/Util/HTTPSignature.php
Normal file
|
@ -0,0 +1,409 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file src/Util/HTTPSignature.php
|
||||||
|
*/
|
||||||
|
namespace Friendica\Util;
|
||||||
|
|
||||||
|
use Friendica\Core\Config;
|
||||||
|
use Friendica\Database\DBM;
|
||||||
|
use Friendica\Util\Crypto;
|
||||||
|
use Friendica\Util\HTTPHeaders;
|
||||||
|
use dba;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Implements HTTP Signatures per draft-cavage-http-signatures-07.
|
||||||
|
*
|
||||||
|
* Ported from Hubzilla: https://framagit.org/hubzilla/core/blob/master/Zotlabs/Web/HTTPSig.php
|
||||||
|
*
|
||||||
|
* @see https://tools.ietf.org/html/draft-cavage-http-signatures-07
|
||||||
|
*/
|
||||||
|
|
||||||
|
class HTTPSignature
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief RFC5843
|
||||||
|
*
|
||||||
|
* Disabled until Friendica's ActivityPub implementation
|
||||||
|
* is ready.
|
||||||
|
*
|
||||||
|
* @see https://tools.ietf.org/html/rfc5843
|
||||||
|
*
|
||||||
|
* @param string $body The value to create the digest for
|
||||||
|
* @param boolean $set (optional, default true)
|
||||||
|
* If set send a Digest HTTP header
|
||||||
|
*
|
||||||
|
* @return string The generated digest of $body
|
||||||
|
*/
|
||||||
|
// public static function generateDigest($body, $set = true)
|
||||||
|
// {
|
||||||
|
// $digest = base64_encode(hash('sha256', $body, true));
|
||||||
|
//
|
||||||
|
// if($set) {
|
||||||
|
// header('Digest: SHA-256=' . $digest);
|
||||||
|
// }
|
||||||
|
// return $digest;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// See draft-cavage-http-signatures-08
|
||||||
|
public static function verify($data, $key = '')
|
||||||
|
{
|
||||||
|
$body = $data;
|
||||||
|
$headers = null;
|
||||||
|
$spoofable = false;
|
||||||
|
$result = [
|
||||||
|
'signer' => '',
|
||||||
|
'header_signed' => false,
|
||||||
|
'header_valid' => false,
|
||||||
|
'content_signed' => false,
|
||||||
|
'content_valid' => false
|
||||||
|
];
|
||||||
|
|
||||||
|
// Decide if $data arrived via controller submission or curl.
|
||||||
|
if (is_array($data) && $data['header']) {
|
||||||
|
if (!$data['success']) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$h = new HTTPHeaders($data['header']);
|
||||||
|
$headers = $h->fetch();
|
||||||
|
$body = $data['body'];
|
||||||
|
} else {
|
||||||
|
$headers = [];
|
||||||
|
$headers['(request-target)'] = strtolower($_SERVER['REQUEST_METHOD']).' '.$_SERVER['REQUEST_URI'];
|
||||||
|
|
||||||
|
foreach ($_SERVER as $k => $v) {
|
||||||
|
if (strpos($k, 'HTTP_') === 0) {
|
||||||
|
$field = str_replace('_', '-', strtolower(substr($k, 5)));
|
||||||
|
$headers[$field] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sig_block = null;
|
||||||
|
|
||||||
|
if (array_key_exists('signature', $headers)) {
|
||||||
|
$sig_block = self::parseSigheader($headers['signature']);
|
||||||
|
} elseif (array_key_exists('authorization', $headers)) {
|
||||||
|
$sig_block = self::parseSigheader($headers['authorization']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$sig_block) {
|
||||||
|
logger('no signature provided.');
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warning: This log statement includes binary data
|
||||||
|
// logger('sig_block: ' . print_r($sig_block,true), LOGGER_DATA);
|
||||||
|
|
||||||
|
$result['header_signed'] = true;
|
||||||
|
|
||||||
|
$signed_headers = $sig_block['headers'];
|
||||||
|
if (!$signed_headers) {
|
||||||
|
$signed_headers = ['date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$signed_data = '';
|
||||||
|
foreach ($signed_headers as $h) {
|
||||||
|
if (array_key_exists($h, $headers)) {
|
||||||
|
$signed_data .= $h . ': ' . $headers[$h] . "\n";
|
||||||
|
}
|
||||||
|
if (strpos($h, '.')) {
|
||||||
|
$spoofable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$signed_data = rtrim($signed_data, "\n");
|
||||||
|
|
||||||
|
$algorithm = null;
|
||||||
|
if ($sig_block['algorithm'] === 'rsa-sha256') {
|
||||||
|
$algorithm = 'sha256';
|
||||||
|
}
|
||||||
|
if ($sig_block['algorithm'] === 'rsa-sha512') {
|
||||||
|
$algorithm = 'sha512';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($key && function_exists($key)) {
|
||||||
|
$result['signer'] = $sig_block['keyId'];
|
||||||
|
$key = $key($sig_block['keyId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't use Activity Pub at the moment.
|
||||||
|
// if (!$key) {
|
||||||
|
// $result['signer'] = $sig_block['keyId'];
|
||||||
|
// $key = self::getActivitypubKey($sig_block['keyId']);
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (!$key) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = Crypto::rsaVerify($signed_data, $sig_block['signature'], $key, $algorithm);
|
||||||
|
|
||||||
|
logger('verified: ' . $x, LOGGER_DEBUG);
|
||||||
|
|
||||||
|
if (!$x) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$spoofable) {
|
||||||
|
$result['header_valid'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('digest', $signed_headers)) {
|
||||||
|
$result['content_signed'] = true;
|
||||||
|
$digest = explode('=', $headers['digest']);
|
||||||
|
|
||||||
|
if ($digest[0] === 'SHA-256') {
|
||||||
|
$hashalg = 'sha256';
|
||||||
|
}
|
||||||
|
if ($digest[0] === 'SHA-512') {
|
||||||
|
$hashalg = 'sha512';
|
||||||
|
}
|
||||||
|
|
||||||
|
// The explode operation will have stripped the '=' padding, so compare against unpadded base64.
|
||||||
|
if (rtrim(base64_encode(hash($hashalg, $body, true)), '=') === $digest[1]) {
|
||||||
|
$result['content_valid'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('Content_Valid: ' . $result['content_valid']);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the public key for Activity Pub contact.
|
||||||
|
*
|
||||||
|
* @param string|int The identifier (contact addr or contact ID).
|
||||||
|
* @return string|boolean The public key or false on failure.
|
||||||
|
*/
|
||||||
|
private static function getActivitypubKey($id)
|
||||||
|
{
|
||||||
|
if (strpos($id, 'acct:') === 0) {
|
||||||
|
$contact = dba::selectFirst('contact', ['pubkey'], ['uid' => 0, 'addr' => str_replace('acct:', '', $id)]);
|
||||||
|
} else {
|
||||||
|
$contact = dba::selectFirst('contact', ['pubkey'], ['id' => $id, 'network' => 'activitypub']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DBM::is_result($contact)) {
|
||||||
|
return $contact['pubkey'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if(function_exists('as_fetch')) {
|
||||||
|
$r = as_fetch($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($r) {
|
||||||
|
$j = json_decode($r, true);
|
||||||
|
|
||||||
|
if (array_key_exists('publicKey', $j) && array_key_exists('publicKeyPem', $j['publicKey'])) {
|
||||||
|
if ((array_key_exists('id', $j['publicKey']) && $j['publicKey']['id'] !== $id) && $j['id'] !== $id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $j['publicKey']['publicKeyPem'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief
|
||||||
|
*
|
||||||
|
* @param string $request
|
||||||
|
* @param array $head
|
||||||
|
* @param string $prvkey
|
||||||
|
* @param string $keyid (optional, default 'Key')
|
||||||
|
* @param boolean $send_headers (optional, default false)
|
||||||
|
* If set send a HTTP header
|
||||||
|
* @param boolean $auth (optional, default false)
|
||||||
|
* @param string $alg (optional, default 'sha256')
|
||||||
|
* @param string $crypt_key (optional, default null)
|
||||||
|
* @param string $crypt_algo (optional, default 'aes256ctr')
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function createSig($request, $head, $prvkey, $keyid = 'Key', $send_headers = false, $auth = false, $alg = 'sha256', $crypt_key = null, $crypt_algo = 'aes256ctr')
|
||||||
|
{
|
||||||
|
$return_headers = [];
|
||||||
|
|
||||||
|
if ($alg === 'sha256') {
|
||||||
|
$algorithm = 'rsa-sha256';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($alg === 'sha512') {
|
||||||
|
$algorithm = 'rsa-sha512';
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = self::sign($request, $head, $prvkey, $alg);
|
||||||
|
|
||||||
|
$headerval = 'keyId="' . $keyid . '",algorithm="' . $algorithm
|
||||||
|
. '",headers="' . $x['headers'] . '",signature="' . $x['signature'] . '"';
|
||||||
|
|
||||||
|
if ($crypt_key) {
|
||||||
|
$x = Crypto::encapsulate($headerval, $crypt_key, $crypt_algo);
|
||||||
|
$headerval = 'iv="' . $x['iv'] . '",key="' . $x['key'] . '",alg="' . $x['alg'] . '",data="' . $x['data'] . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($auth) {
|
||||||
|
$sighead = 'Authorization: Signature ' . $headerval;
|
||||||
|
} else {
|
||||||
|
$sighead = 'Signature: ' . $headerval;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($head) {
|
||||||
|
foreach ($head as $k => $v) {
|
||||||
|
if ($send_headers) {
|
||||||
|
// This is for ActivityPub implementation.
|
||||||
|
// Since the Activity Pub implementation isn't
|
||||||
|
// ready at the moment, we comment it out.
|
||||||
|
// header($k . ': ' . $v);
|
||||||
|
} else {
|
||||||
|
$return_headers[] = $k . ': ' . $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($send_headers) {
|
||||||
|
// This is for ActivityPub implementation.
|
||||||
|
// Since the Activity Pub implementation isn't
|
||||||
|
// ready at the moment, we comment it out.
|
||||||
|
// header($sighead);
|
||||||
|
} else {
|
||||||
|
$return_headers[] = $sighead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $return_headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief
|
||||||
|
*
|
||||||
|
* @param string $request
|
||||||
|
* @param array $head
|
||||||
|
* @param string $prvkey
|
||||||
|
* @param string $alg (optional) default 'sha256'
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private static function sign($request, $head, $prvkey, $alg = 'sha256')
|
||||||
|
{
|
||||||
|
$ret = [];
|
||||||
|
$headers = '';
|
||||||
|
$fields = '';
|
||||||
|
|
||||||
|
if ($request) {
|
||||||
|
$headers = '(request-target)' . ': ' . trim($request) . "\n";
|
||||||
|
$fields = '(request-target)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($head) {
|
||||||
|
foreach ($head as $k => $v) {
|
||||||
|
$headers .= strtolower($k) . ': ' . trim($v) . "\n";
|
||||||
|
if ($fields) {
|
||||||
|
$fields .= ' ';
|
||||||
|
}
|
||||||
|
$fields .= strtolower($k);
|
||||||
|
}
|
||||||
|
// strip the trailing linefeed
|
||||||
|
$headers = rtrim($headers, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$sig = base64_encode(Crypto::rsaSign($headers, $prvkey, $alg));
|
||||||
|
|
||||||
|
$ret['headers'] = $fields;
|
||||||
|
$ret['signature'] = $sig;
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief
|
||||||
|
*
|
||||||
|
* @param string $header
|
||||||
|
* @return array associate array with
|
||||||
|
* - \e string \b keyID
|
||||||
|
* - \e string \b algorithm
|
||||||
|
* - \e array \b headers
|
||||||
|
* - \e string \b signature
|
||||||
|
*/
|
||||||
|
public static function parseSigheader($header)
|
||||||
|
{
|
||||||
|
$ret = [];
|
||||||
|
$matches = [];
|
||||||
|
|
||||||
|
// if the header is encrypted, decrypt with (default) site private key and continue
|
||||||
|
if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$header = self::decryptSigheader($header);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/keyId="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$ret['keyId'] = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/algorithm="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$ret['algorithm'] = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/headers="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$ret['headers'] = explode(' ', $matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/signature="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$ret['signature'] = base64_decode(preg_replace('/\s+/', '', $matches[1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($ret['signature']) && ($ret['algorithm']) && (!$ret['headers'])) {
|
||||||
|
$ret['headers'] = ['date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief
|
||||||
|
*
|
||||||
|
* @param string $header
|
||||||
|
* @param string $prvkey (optional), if not set use site private key
|
||||||
|
*
|
||||||
|
* @return array|string associative array, empty string if failue
|
||||||
|
* - \e string \b iv
|
||||||
|
* - \e string \b key
|
||||||
|
* - \e string \b alg
|
||||||
|
* - \e string \b data
|
||||||
|
*/
|
||||||
|
private static function decryptSigheader($header, $prvkey = null)
|
||||||
|
{
|
||||||
|
$iv = $key = $alg = $data = null;
|
||||||
|
|
||||||
|
if (!$prvkey) {
|
||||||
|
$prvkey = Config::get('system', 'prvkey');
|
||||||
|
}
|
||||||
|
|
||||||
|
$matches = [];
|
||||||
|
|
||||||
|
if (preg_match('/iv="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$iv = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/key="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$key = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/alg="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$alg = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/data="(.*?)"/ism', $header, $matches)) {
|
||||||
|
$data = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iv && $key && $alg && $data) {
|
||||||
|
return Crypto::unencapsulate(['iv' => $iv, 'key' => $key, 'alg' => $alg, 'data' => $data], $prvkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,4 +33,7 @@
|
||||||
template="{{$subscribe}}" />
|
template="{{$subscribe}}" />
|
||||||
<Link rel="magic-public-key"
|
<Link rel="magic-public-key"
|
||||||
href="{{$modexp}}" />
|
href="{{$modexp}}" />
|
||||||
|
<Link rel="http://purl.org/openwebauth/v1"
|
||||||
|
type="application/x-dfrn+json"
|
||||||
|
href="{{$openwebauth}}" />
|
||||||
</XRD>
|
</XRD>
|
||||||
|
|
Loading…
Reference in a new issue