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 inboxesmerge-requests/5/head
rodzic
89539317ba
commit
2a694f8290
|
@ -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
221
index.php
|
@ -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) );
|
||||
|
|
Ładowanie…
Reference in New Issue