From 6b609a487def52a3830ed5878fc84ac1fef4c1be Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:31:42 +0100 Subject: [PATCH 01/19] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f34186f..75d0b2a 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,8 @@ creates a Note (the default) with the text "Hello, world!" ``` etc. + +# Nigel Whitfield updates, July 2023 +This fork provides an additional profile page within admin, allowing youto populate some of the information that will be displayed when people search for your actor. + +It additionally addresses a couple of minor issues relevant to my own installation, viz database creation, and an Apache re-write. From 69ed4ceaa603448de251913be3d36cd08f26353a Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:32:55 +0100 Subject: [PATCH 02/19] Add profile management page --- admin/profile.php | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 admin/profile.php diff --git a/admin/profile.php b/admin/profile.php new file mode 100644 index 0000000..fa3f3d3 --- /dev/null +++ b/admin/profile.php @@ -0,0 +1,104 @@ +close() ; + + + header('Location: index.php') ; +} + +$profile = query($db, 'SELECT summary, url, name, icon, homepage FROM profile WHERE user=?', $_REQUEST['user']); +$db->close() ; + +?> + + +
++ Back to index +
+ + + \ No newline at end of file From 6b88dcd22852b31d5a539a4561ae093b1fb38fa9 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:34:01 +0100 Subject: [PATCH 03/19] Add files via upload Add additional profile fields to repsonse, if found in database --- actor.php | 137 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 50 deletions(-) diff --git a/actor.php b/actor.php index c9788dd..023eae3 100644 --- a/actor.php +++ b/actor.php @@ -50,59 +50,96 @@ // check query parameters and extract userpart if (! empty($_GET['user'])) { - if (strtolower($_GET['user']) === $_SERVER['SERVER_NAME']) { - // instance actor - always served without needing signature - $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); - $info = query($db, 'SELECT rsa_public FROM instance'); - $db->close(); - - if (count($info) > 0) { - // The instance actor has been set up properly. - $user = $_SERVER['SERVER_NAME']; - $type = 'Application'; - $rsa_public = $info[0]['rsa_public']; - } else { - response(500, [ 'error' => 'Instance actor has not been properly configured.' ]); - } + if (strtolower($_GET['user']) === $_SERVER['SERVER_NAME']) { + // instance actor - always served without needing signature + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $info = query($db, 'SELECT rsa_public FROM instance'); + $db->close(); + + if (count($info) > 0) { + // The instance actor has been set up properly. + $user = $_SERVER['SERVER_NAME']; + $type = 'Application'; + $rsa_public = $info[0]['rsa_public']; + } else { + response(500, [ 'error' => 'Instance actor has not been properly configured.' ]); + } + } else { + // TODO: Verify signature + + // connect to db, look up user info (verify the actor exists) + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $info = query($db, 'SELECT user, type, rsa_public FROM acct WHERE user=?', $_GET['user']); + $db->close(); + + if (count($info) > 0) { + // Found an account by this name. + $user = $info[0]['user']; + $type = $info[0]['type']; + $rsa_public = $info[0]['rsa_public']; } else { - // TODO: Verify signature - - // connect to db, look up user info (verify the actor exists) - $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); - $info = query($db, 'SELECT user, type, rsa_public FROM acct WHERE user=?', $_GET['user']); - $db->close(); - - if (count($info) > 0) { - // Found an account by this name. - $user = $info[0]['user']; - $type = $info[0]['type']; - $rsa_public = $info[0]['rsa_public']; - } else { - response(404, [ 'error' => 'No such user ' . $_GET['user'] . ' in Actor request' ]); - } + response(404, [ 'error' => 'No such user ' . $_GET['user'] . ' in Actor request' ]); } + } // Build the complete Actor doc and send it out. - $actor = [ - "@context" => ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"], - "id" => $phpActivityPub_root . 'actor.php?user=' . $user, - "type" => $type, - - "inbox" => $phpActivityPub_root . 'inbox.php?user=' . $user, - "outbox" => $phpActivityPub_root . 'outbox.php?user=' . $user, - - "preferredUsername" => $user, - "followers" => $phpActivityPub_root . 'followers.php?user=' . $user, - "following" => $phpActivityPub_root . 'following.php?user=' . $user, - - "publicKey" => [ - "id" => $phpActivityPub_root . 'actor.php?user=' . $user . '#main-key', - "owner" => $phpActivityPub_root . 'actor.php?user=' . $user, - "publicKeyPem" => $rsa_public - ] - ]; - - response(200, $actor); + $actor = [ + "@context" => ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"], + "id" => $phpActivityPub_root . 'actor.php?user=' . $user, + "type" => $type, + + "inbox" => $phpActivityPub_root . 'inbox.php?user=' . $user, + "outbox" => $phpActivityPub_root . 'outbox.php?user=' . $user, + + "preferredUsername" => $user, + + "followers" => $phpActivityPub_root . 'followers.php?user=' . $user, + "following" => $phpActivityPub_root . 'following.php?user=' . $user, + + "publicKey" => [ + "id" => $phpActivityPub_root . 'actor.php?user=' . $user . '#main-key', + "owner" => $phpActivityPub_root . 'actor.php?user=' . $user, + "publicKeyPem" => $rsa_public + ] + ]; + + // NW: check for extended profile information + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $profile = query($db, 'SELECT summary, url, name, icon, homepage FROM profile WHERE user=?', $_GET['user']); + $db->close() ; + + if (count($profile) > 0) { + // there is profile info for this user + if (strlen($profile[0]['summary']) > 0) { + $actor['summary'] = $profile[0]['summary'] ; + } + if (strlen($profile[0]['url']) > 0) { + $actor['url'] = $profile[0]['url'] ; + } + if (strlen($profile[0]['name']) > 0) { + $actor['name'] = $profile[0]['name'] ; + } + if (strlen($profile[0]['icon']) > 0) { + $icon = [ + "type" => "Image", + "mediaType" => "image/png", + "url" => $profile[0]['icon'] + ] ; + $actor['icon'] = $icon ; + } + if (strlen($profile[0]['homepage']) > 0) { + $homepage = [ + "type" => "PropertyValue", + "name" => "Homepage", + "value" => "" . preg_replace('#http(s)?://#', '', $profile[0]['homepage']) . "" + ] ; + + $actor['attachment'] = [ $homepage ] ; + } + } + + + response(200, $actor); } else { - response(400, [ 'error' => 'User missing from Actor request' ]); + response(400, [ 'error' => 'User missing from Actor request' ]); } From fa18edd4fd32b7fede955b49068485b7a731b7da Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:35:46 +0100 Subject: [PATCH 04/19] Handle Apache Rewrite rule Return the correct document path when webfinger is handled via an Apache re-write rule --- admin/functions.php | 219 +++++++++++++++++++++++--------------------- 1 file changed, 115 insertions(+), 104 deletions(-) diff --git a/admin/functions.php b/admin/functions.php index c568d85..193c0aa 100644 --- a/admin/functions.php +++ b/admin/functions.php @@ -10,12 +10,23 @@ // SERVER_NAME is only defined when invoked through CGI. // You may set a value here that be used for CLI calls. if (empty($_SERVER['SERVER_NAME'])) { - $_SERVER['SERVER_NAME'] = 'example.com'; + $_SERVER['SERVER_NAME'] = 'example.com'; } // Sets the "root" of phpActivityPub - that is, the hostname plus path to the // PHP scripts (but not the scripts themselves) -$phpActivityPub_root = 'https://' . $_SERVER['SERVER_NAME'] . preg_replace('#[^/]+\.php$#', '', $_SERVER['SCRIPT_NAME']); + +// NW; check to handle webfinger being redirected via Apache +// for example, we have a rewrite rule like this: +// +// RewriteRule ^/.well-known/webfinger /activitypub/webfinger.php [L,QSA] +// +if (preg_match('#/webfinger.php$#', $_SERVER['SCRIPT_FILENAME'])) { + $filepath = preg_replace('#[^/]+\.php$#', '', $_SERVER['SCRIPT_FILENAME']); + $phpActivityPub_root = 'https://' . $_SERVER['SERVER_NAME'] . preg_replace('#' . $_SERVER['DOCUMENT_ROOT'] .'#', '', $filepath); +} else { + $phpActivityPub_root = 'https://' . $_SERVER['SERVER_NAME'] . preg_replace('#[^/]+\.php$#', '', $_SERVER['SCRIPT_NAME']); +} // //////////////// // TEMPLATE @@ -23,12 +34,12 @@ // Send a JSON response and a status code, then exit function response($code = 204, $object = null) { - http_response_code($code); - if ($object) { - header('Content-Type: application/json'); - echo json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - } - exit; + http_response_code($code); + if ($object) { + header('Content-Type: application/json'); + echo json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + exit; } // //////////////// @@ -38,33 +49,33 @@ function response($code = 204, $object = null) function query($db, $sql, ...$params) { // prepare query and bind parameters - $stmt = $db->prepare($sql); + $stmt = $db->prepare($sql); // check that the right number of params were passed in - if ($stmt->paramCount() != count($params)) { - throw new Exception("Error in query '$sql': expected " . $stmt->paramCount() . " but got " . count($params)); - } + if ($stmt->paramCount() != count($params)) { + throw new Exception("Error in query '$sql': expected " . $stmt->paramCount() . " but got " . count($params)); + } - for ($i = 0; $i < count($params); $i++) { - $stmt->bindParam($i + 1, $params[$i], gettype($params[$i]) == 'integer' ? SQLITE3_INTEGER : SQLITE3_TEXT); - } + for ($i = 0; $i < count($params); $i++) { + $stmt->bindParam($i + 1, $params[$i], gettype($params[$i]) == 'integer' ? SQLITE3_INTEGER : SQLITE3_TEXT); + } - $result = $stmt->execute(); + $result = $stmt->execute(); - $response = array(); + $response = array(); // a FALSE value here might indicate a db error but we should not explode // e.g. in case of duplicate key or something non-fatal - if ($result) { - // retrieve all responses - while ($info = $result->fetchArray(SQLITE3_ASSOC)) { - array_push($response, $info); - } - $result->finalize(); + if ($result) { + // retrieve all responses + while ($info = $result->fetchArray(SQLITE3_ASSOC)) { + array_push($response, $info); } + $result->finalize(); + } // close everything and return - $stmt->close(); + $stmt->close(); - return $response; + return $response; } // //////////////// @@ -78,96 +89,96 @@ function request($user, $url, $body = null) { // fetch the private key: we need these to encode the message headers - $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); - $result = query($db, 'SELECT rsa_private FROM acct WHERE user=?', $user); - if (count($result) == 1) { - $keypair = $result[0]['rsa_private']; - } else { - throw new Exception("Failed to get rsa_private for user $user"); - } - $db->close(); + $db = new SQLite3("admin/db.sqlite3", SQLITE3_OPEN_READONLY); + $result = query($db, 'SELECT rsa_private FROM acct WHERE user=?', $user); + if (count($result) == 1) { + $keypair = $result[0]['rsa_private']; + } else { + throw new Exception("Failed to get rsa_private for user $user"); + } + $db->close(); // Break the URL into components for putting into headers - $components = parse_url($url); + $components = parse_url($url); // Build headers and signature string // get today's date as well - $date = gmdate('D, d M Y H:i:s T'); + $date = gmdate('D, d M Y H:i:s T'); // concatenate it all together - $signed_string = "(request-target): " . ($body ? 'post' : 'get') . ' ' . $components['path'] . - "\nhost: " . $components['host'] . - "\ndate: " . $date; - if ($body) { - // create digest of the document - $digest = 'SHA-256=' . base64_encode(hash('sha256', $body, true)); - $signed_string .= "\ndigest: " . $digest; - } + $signed_string = "(request-target): " . ($body ? 'post' : 'get') . ' ' . $components['path'] . + "\nhost: " . $components['host'] . + "\ndate: " . $date; + if ($body) { + // create digest of the document + $digest = 'SHA-256=' . base64_encode(hash('sha256', $body, true)); + $signed_string .= "\ndigest: " . $digest; + } // SIGN THE HEADERS - openssl_sign($signed_string, $signature, $keypair, OPENSSL_ALGO_SHA256); + openssl_sign($signed_string, $signature, $keypair, OPENSSL_ALGO_SHA256); // Create the final header with signature - $header = 'keyId="' . $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user . '#main-key",headers="(request-target) host date' . ($body ? ' digest' : '') . '",signature="' . base64_encode($signature) . '"'; + $header = 'keyId="' . $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user . '#main-key",headers="(request-target) host date' . ($body ? ' digest' : '') . '",signature="' . base64_encode($signature) . '"'; // READY TO POST! //open connection - $ch = curl_init($url); - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_MAXREDIRS, 50); - - #'Host: ' . $components['host'], - $headers = array( - 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Date: ' . $date, - 'Signature: ' . $header + $ch = curl_init($url); + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_MAXREDIRS, 50); + + #'Host: ' . $components['host'], + $headers = array( + 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Date: ' . $date, + 'Signature: ' . $header + ); + + if ($body) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + array_push( + $headers, + 'Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Digest: ' . $digest ); + }; - if ($body) { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - array_push( - $headers, - 'Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Digest: ' . $digest - ); - }; - - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - - error_log("REQUEST: $url, headers:" . print_r($headers, true)); - if ($body) { - error_log("body: $body"); - } - - //execute the request! - $result = curl_exec($ch); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - if ($result === false) { - $e = curl_errno($ch); - $err_str = "cURL error: ($e: " . curl_strerror($e) . "): " . curl_error($ch); - curl_close($ch); + error_log("REQUEST: $url, headers:" . print_r($headers, true)); + if ($body) { + error_log("body: $body"); + } - throw new Exception($err_str); - } + //execute the request! + $result = curl_exec($ch); - $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + if ($result === false) { + $e = curl_errno($ch); + $err_str = "cURL error: ($e: " . curl_strerror($e) . "): " . curl_error($ch); curl_close($ch); - if ($responseCode < 200 || $responseCode >= 300) { - throw new Exception("cURL error: Server responded $responseCode: $result"); - } - if ($result) { - error_log("Received result: $result"); - $json_response = json_decode($result, true); - if (! $json_response) { - throw new Exception("json_decode error: Failed to decode '$result'"); - } - return $json_response; + + throw new Exception($err_str); + } + + $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + curl_close($ch); + if ($responseCode < 200 || $responseCode >= 300) { + throw new Exception("cURL error: Server responded $responseCode: $result"); + } + if ($result) { + error_log("Received result: $result"); + $json_response = json_decode($result, true); + if (! $json_response) { + throw new Exception("json_decode error: Failed to decode '$result'"); } - return array(); + return $json_response; + } + return array(); } // Given an Actor URL, get some info we need to post to them @@ -177,13 +188,13 @@ function request($user, $url, $body = null) function get_inbox($user, $url) { // use the instance actor for this req - $json_content = request($user, $url); + $json_content = request($user, $url); - if ($json_content['inbox']) { - return $json_content['inbox']; - } + if ($json_content['inbox']) { + return $json_content['inbox']; + } - throw new Exception("Failed to look up actor for " . $url . ": " . $json_content); + throw new Exception("Failed to look up actor for " . $url . ": " . $json_content); } // Send an Activity to a remote server. @@ -192,12 +203,12 @@ function sendActivity($user, $dest, $activity) { // right FIRST of all, we need the destination inbox - $dest = get_inbox($user, $dest); + $dest = get_inbox($user, $dest); - $document = json_encode(array_merge([ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'actor' => $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user - ], $activity), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $document = json_encode(array_merge([ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'actor' => $GLOBALS['phpActivityPub_root'] . 'actor.php?user=' . $user + ], $activity), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - return request($user, $dest, $document); + return request($user, $dest, $document); } From 729cfcb4cb19a6f9048c389d8165e63f5abb6fa5 Mon Sep 17 00:00:00 2001 From: Nigel Whitfield <42769531+nigelwhitfield@users.noreply.github.com> Date: Wed, 5 Jul 2023 14:36:22 +0100 Subject: [PATCH 05/19] Add profile link to index page --- admin/index.php | 87 ++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/admin/index.php b/admin/index.php index 795868d..fbf1733 100644 --- a/admin/index.php +++ b/admin/index.php @@ -1,17 +1,21 @@ - -| User | Type | Post Key |
|---|
| User | +Type | +Post Key | +', $a['user'], ' | ', $a['type'], ' | ', $a['key'], " | \n"; + // NW - added link to profile script + echo '
|---|---|---|---|
| ', $a['user'], ' | ', $a['type'], ' | ', $a['key'], " | Profile |