Use correct inboxes

This is a BIG change.

* Receive a follow request
* Sign a request back to the follower to get their details
* Save their details to disk
* Read their inbox
* Sign an Accept message and post to their inbox

On posting

* Read all the followers' details
* Get all shared inboxes or, if they don't exist, unique inboxes
* Sign and post to those inboxes
merge-requests/5/head
Terence Eden 2024-02-23 15:08:25 +00:00
rodzic 89539317ba
commit 2a694f8290
2 zmienionych plików z 146 dodań i 76 usunięć

Wyświetl plik

@ -24,6 +24,7 @@ There are no tests, no checks, no security features, no header verifications, no
* The `index.php` file performs a specific action depending on the path requested.
* Log files are saved as .txt in the `/logs` directory.
* Post files are saved as .json in the `/posts` directory.
* Followers' details are saved as .json in the `/data/followers` directory.
* This has sloppy support for linking #hashtags, https:// URls, and @ mentions.
## Requirements

221
index.php
Wyświetl plik

@ -215,29 +215,57 @@
if ( "Follow" != $inbox_type ) { die(); }
// Get the parameters
$inbox_id = $inbox_message["id"];
$inbox_actor = $inbox_message["actor"];
$inbox_host = parse_url( $inbox_actor, PHP_URL_HOST );
$follower_id = $inbox_message["id"];
$follower_actor = $inbox_message["actor"];
$follower_host = parse_url( $follower_actor, PHP_URL_HOST );
$follower_path = parse_url( $follower_actor, PHP_URL_PATH );
// Does this account have any followers?
if( file_exists( "followers.json" ) ) {
$followers_file = file_get_contents( "followers.json" );
$followers_json = json_decode( $followers_file, true );
// Get the actor's profile as JSON
// Is the actor an https URl?
if(
( filter_var( $follower_actor, FILTER_VALIDATE_URL) == true) &&
( parse_url( $follower_actor, PHP_URL_SCHEME ) == "https" )
) {
// Request the JSON representation of the the user
$ch = curl_init( $follower_actor );
// Get the signed headers
$headers = generate_signed_headers( null, $follower_host, $follower_path, "GET" );
// Set cURL options
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
// Execute the cURL session
$inbox_actor_json = curl_exec( $ch );
// Check for errors
if (curl_errno($ch)) {
// Handle cURL error
die();
}
// Close cURL session
curl_close($ch);
// Save the actor's data in `/data/followers/`
if( ! is_dir( "data/followers" ) ) { mkdir( "data"); mkdir( "data/followers"); }
$follower_filename = urlencode( $follower_actor );
file_put_contents( "data/followers/{$follower_filename}.json", $inbox_actor_json );
} else {
$followers_json = array();
die();
}
// Add user to list. Don't care about duplicate users, server is what's important
$followers_json[$inbox_host]["users"][] = $inbox_actor;
// Save the new followers file
file_put_contents( "followers.json", print_r( json_encode( $followers_json ), true ) );
// Get the new follower's Inbox
$follower_actor_details = json_decode( $inbox_actor_json, true );
$follower_inbox = $follower_actor_details["inbox"];
// Response Message ID
// This isn't used for anything important so could just be a random number
$guid = uuid();
// Create the Accept message
// Create the Accept message to the new follower
$message = [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "https://{$server}/{$guid}",
@ -245,27 +273,20 @@
"actor" => "https://{$server}/{$username}",
"object" => [
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => $inbox_id,
"id" => $follower_id,
"type" => $inbox_type,
"actor" => $inbox_actor,
"actor" => $follower_actor,
"object" => "https://{$server}/{$username}",
]
];
// The Accept is sent to the server of the user who requested the follow
// TODO: The path doesn't *always* end with /inbox
$host = $inbox_host;
$path = parse_url( $inbox_actor, PHP_URL_PATH ) . "/inbox";
// The Accept is POSTed to the inbox on the server of the user who requested the follow
$follower_inbox_path = parse_url( $follower_inbox, PHP_URL_PATH );
// Get the signed headers
$headers = generate_signed_headers( $message, $host, $path );
$headers = generate_signed_headers( $message, $follower_host, $follower_inbox_path, "POST" );
// Specify the URL of the remote server's inbox
// TODO: The path doesn't *always* end with /inbox
$remoteServerUrl = $inbox_actor . "/inbox";
// POST the message and header to the requester's inbox
$ch = curl_init( $remoteServerUrl );
$ch = curl_init( $follower_inbox );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode($message) );
@ -297,49 +318,79 @@
// Headers:
// Every message that your server sends needs to be cryptographically signed with your Private Key.
// This is a complicated process. Please read https://blog.joinmastodon.org/2018/07/how-to-make-friends-and-verify-requests/ for more information.
function generate_signed_headers( $message, $host, $path ) {
function generate_signed_headers( $message, $host, $path, $method ) {
global $server, $username, $key_private;
// Encode the message object to JSON
$message_json = json_encode( $message );
// Location of the Public Key
$keyId = "https://{$server}/{$username}#main-key";
// Generate signing variables
$hash = hash( "sha256", $message_json, true );
$digest = base64_encode( $hash );
$date = date( "D, d M Y H:i:s \G\M\T" );
// Get the Private Key
$signer = openssl_get_privatekey( $key_private );
// Sign the path, host, date, and digest
$stringToSign = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest";
// The signing function returns the variable $signature
// https://www.php.net/manual/en/function.openssl-sign.php
openssl_sign(
$stringToSign,
$signature,
$signer,
OPENSSL_ALGO_SHA256
);
// Encode the signature
$signature_b64 = base64_encode( $signature );
// Timestamp this message was sent
$date = date( "D, d M Y H:i:s \G\M\T" );
// Full signature header
$signature_header = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
// There are subtly different signing requirements for POST and GET
// GET does not require a digest
if ( "POST" == $method ) {
// Encode the message object to JSON
$message_json = json_encode( $message );
// Generate signing variables
$hash = hash( "sha256", $message_json, true );
$digest = base64_encode( $hash );
// Header for POST reply
$headers = array(
"Host: {$host}",
"Date: {$date}",
"Digest: SHA-256={$digest}",
"Signature: {$signature_header}",
"Content-Type: application/activity+json",
"Accept: application/activity+json",
);
// Sign the path, host, date, and digest
$stringToSign = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest";
// The signing function returns the variable $signature
// https://www.php.net/manual/en/function.openssl-sign.php
openssl_sign(
$stringToSign,
$signature,
$signer,
OPENSSL_ALGO_SHA256
);
// Encode the signature
$signature_b64 = base64_encode( $signature );
// Full signature header
$signature_header = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
// Header for POST reply
$headers = array(
"Host: {$host}",
"Date: {$date}",
"Digest: SHA-256={$digest}",
"Signature: {$signature_header}",
"Content-Type: application/activity+json",
"Accept: application/activity+json",
);
} else if ( "GET" == $method ) {
// Sign the path, host, date - NO DIGEST
$stringToSign = "(request-target): get $path\nhost: $host\ndate: $date";
// The signing function returns the variable $signature
// https://www.php.net/manual/en/function.openssl-sign.php
openssl_sign(
$stringToSign,
$signature,
$signer,
OPENSSL_ALGO_SHA256
);
// Encode the signature
$signature_b64 = base64_encode( $signature );
// Full signature header
$signature_header = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date",signature="' . $signature_b64 . '"';
// Header for POST reply
$headers = array(
"Host: {$host}",
"Date: {$date}",
"Signature: {$signature_header}",
"Accept: application/activity+json",
);
}
return $headers;
}
@ -406,7 +457,9 @@ HTML;
// Send Endpoint:
// This takes the submitted message and checks the password is correct.
// It reads the `followers.json` file and sends the message to every server that is following this account.
// It reads all the followers' data in `data/followers`
// It constructs a list of shared inboxes and unique inboxes
// It sends the message to every server that is following this account.
function send() {
global $password, $server, $username, $key_private;
@ -505,29 +558,45 @@ HTML;
if( ! is_dir( "posts" ) ) { mkdir( "posts"); }
file_put_contents( "posts/{$guid}.json", print_r( $note_json, true ) );
// Read existing users and get their hosts
$followers_file = file_get_contents( "followers.json" );
$followers_json = json_decode( $followers_file, true );
$hosts = array_keys( $followers_json );
// Read existing followers
$followers = glob( "data/followers/*.json" );
// Get all the inboxes
$inboxes = [];
foreach ( $followers as $follower ) {
$follower_info = json_decode( file_get_contents( $follower ), true );
// Shared inboxes
if( isset( $follower_info["endpoints"]["sharedInbox"] ) ) {
$sharedInbox = $follower_info["endpoints"]["sharedInbox"];
if ( !in_array( $sharedInbox, $inboxes ) ) {
$inboxes[] = $sharedInbox;
}
} else {
// If not, use the individual inbox
$inbox = $follower_info["inbox"];
if ( !in_array( $inbox, $inboxes ) ) {
$inboxes[] = $inbox;
}
}
}
// Prepare to use the multiple cURL handle
$mh = curl_multi_init();
// Loop through all the severs of the followers
// Loop through all the inboxes of the followers
// Each server needs its own cURL handle
// Each POST to an inbox needs to be signed separately
foreach ( $hosts as $host ) {
// TODO: Not every host uses /inbox
$path = "/inbox";
foreach ( $inboxes as $inbox ) {
$inbox_host = parse_url( $inbox, PHP_URL_HOST );
$inbox_path = parse_url( $inbox, PHP_URL_PATH );
// Get the signed headers
$headers = generate_signed_headers( $message, $host, $path );
// Specify the URL of the remote server
$remoteServerUrl = "https://{$host}{$path}";
$headers = generate_signed_headers( $message, $inbox_host, $inbox_path, "POST" );
// POST the message and header to the requester's inbox
$ch = curl_init( $remoteServerUrl );
$ch = curl_init( $inbox );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "POST" );
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode($message) );