Tidy documentation etc

merge-requests/5/head
Terence Eden 2024-03-01 23:26:10 +00:00
rodzic 69b0df6e87
commit e98623624f
1 zmienionych plików z 60 dodań i 44 usunięć

104
index.php
Wyświetl plik

@ -9,6 +9,7 @@
* The Actor can send messages to followers. * The Actor can send messages to followers.
* The message can have linkable URls, hashtags, and mentions. * The message can have linkable URls, hashtags, and mentions.
* An image and alt text can be attached to the message. * An image and alt text can be attached to the message.
* The Actor can follow remote accounts.
* The Server saves logs about requests it receives and sends. * The Server saves logs about requests it receives and sends.
* This code is NOT suitable for production use. * This code is NOT suitable for production use.
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
@ -36,26 +37,29 @@
// Password for sending messages // Password for sending messages
$password = "P4ssW0rd"; $password = "P4ssW0rd";
/** No need to edit anything below here. **/ /** No need to edit anything below here. But please go exploring! **/
// Internal data // Internal data
$server = $_SERVER["SERVER_NAME"]; // Do not change this! $server = $_SERVER["SERVER_NAME"]; // Do not change this!
// Set up where logs and messages go // Set up where logs and messages go.
// If you want to, you can change these directories to something more suitable for you.
$data = "data"; $data = "data";
$directories = array( $directories = array(
"inbox" => "{$data}/inbox", "inbox" => "{$data}/inbox",
"followers" => "{$data}/followers", "followers" => "{$data}/followers",
"following" => "{$data}/following", "following" => "{$data}/following",
"logs" => "{$data}/logs", "logs" => "{$data}/logs",
"posts" => "posts", "posts" => "posts",
"images" => "images",
); );
foreach ( $directories as $directory ) { foreach ( $directories as $directory ) {
if( !is_dir( $directory ) ) { mkdir( $data ); mkdir( $directory ); } if( !is_dir( $directory ) ) { mkdir( $data ); mkdir( $directory ); }
} }
// Logging: // Logging:
// ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them as a datestamped text file. // ActivityPub is a "chatty" protocol.
// This takes all the requests your server receives and saves them as a datestamped text file.
// Get all headers and requests sent to this server // Get all headers and requests sent to this server
$headers = print_r( getallheaders(), true ); $headers = print_r( getallheaders(), true );
@ -72,19 +76,23 @@
// Get the type of request - used in the log filename // Get the type of request - used in the log filename
if ( isset( $body["type"] ) ) { if ( isset( $body["type"] ) ) {
// Sanitise before using it in a filename // Sanitise before using it in a filename
$type = "." . urlencode( $body["type"] ); $type = urlencode( $body["type"] );
} else { } else {
// Sanitise the path requested // Sanitise the path requested
$type = "." . urlencode( $path ); $type = urlencode( $path );
} }
// Create a timestamp for the filename // Create a timestamp for the filename.
// This format has milliseconds, so should avoid logs being overwritten. // This format has milliseconds, so should avoid logs being overwritten.
// If you have > 1000 requests per second, please use a different server. // If you have > 1000 requests per second, please use a different server.
$timestamp = ( new DateTime() )->format( DATE_RFC3339_EXTENDED ); $timestamp = ( new DateTime() )->format( DATE_RFC3339_EXTENDED );
// Alternatively, use a UUID. These retain the date as hex encoded UNIX seconds.
// This means the log files will never clash - but it does make the filenames harder to read.
// $timestamp = uuid();
// Filename for the log // Filename for the log
$filename = "{$timestamp}{$type}.txt"; $filename = "{$timestamp}.{$type}.txt";
// Save headers and request data to the timestamped file in the logs directory // Save headers and request data to the timestamped file in the logs directory
file_put_contents( $directories["logs"] . "/{$filename}", file_put_contents( $directories["logs"] . "/{$filename}",
@ -106,11 +114,11 @@
case rawurldecode( $username ): case rawurldecode( $username ):
username(); // Mandatory. Static username(); // Mandatory. Static
case "following": case "following":
following(); // Mandatory. Static following(); // Mandatory. Can be static or dynamic.
case "followers": case "followers":
followers(); // Mandatory. Can be dynamic followers(); // Mandatory. Can be static or dynamic.
case "inbox": case "inbox":
inbox(); // Mandatory. Only accepts follow requests. inbox(); // Mandatory.
case "write": case "write":
write(); // User interface for writing posts write(); // User interface for writing posts
case "send": case "send":
@ -197,9 +205,10 @@
// Get all the files // Get all the files
$following_files = glob( $directories["following"] . "/*.json"); $following_files = glob( $directories["following"] . "/*.json");
// Number of posts // Number of users
$totalItems = count( $following_files ); $totalItems = count( $following_files );
// Create a list of all the followers
$items = array(); $items = array();
foreach ( $following_files as $following_file ) { foreach ( $following_files as $following_file ) {
$following = json_decode( file_get_contents( $following_file ),true ); $following = json_decode( file_get_contents( $following_file ),true );
@ -224,9 +233,10 @@
// Get all the files // Get all the files
$follower_files = glob( $directories["followers"] . "/*.json"); $follower_files = glob( $directories["followers"] . "/*.json");
// Number of posts // Number of users
$totalItems = count( $follower_files ); $totalItems = count( $follower_files );
// Create a list of everyone being followed
$items = array(); $items = array();
foreach ( $follower_files as $follower_file ) { foreach ( $follower_files as $follower_file ) {
$following = json_decode( file_get_contents( $follower_file ),true ); $following = json_decode( file_get_contents( $follower_file ),true );
@ -247,10 +257,6 @@
// Inbox: // Inbox:
// The `/inbox` is the main server. It receives all requests. // The `/inbox` is the main server. It receives all requests.
// This server only responds to "Follow" requests.
// A remote server sends a follow request which is a JSON file saying who they are.
// The details of the remote user's server is saved to a file so that future messages can be delivered to the follower.
// An accept request is cryptographically signed and POST'd back to the remote server.
function inbox() { function inbox() {
global $body, $server, $username, $key_private, $directories; global $body, $server, $username, $key_private, $directories;
@ -274,15 +280,17 @@
file_put_contents( $directories["inbox"] . "/{$inbox_filename}", json_encode( $inbox_message ) ); file_put_contents( $directories["inbox"] . "/{$inbox_filename}", json_encode( $inbox_message ) );
} }
// This inbox only responds to follow requests.
// This inbox only responds to follow requests // A remote server sends the inbox follow request which is a JSON file saying who they are.
// The details of the remote user's server is saved to a file so that future messages can be delivered to the follower.
// An accept request is cryptographically signed and POST'd back to the remote server.
if ( "Follow" != $inbox_type ) { die(); } if ( "Follow" != $inbox_type ) { die(); }
// Get the parameters // Get the parameters
$follower_id = $inbox_message["id"]; // E.g. https://mastodon.social/(unique id) $follower_id = $inbox_message["id"]; // E.g. https://mastodon.social/(unique id)
$follower_actor = $inbox_message["actor"]; // E.g. https://mastodon.social/users/Edent $follower_actor = $inbox_message["actor"]; // E.g. https://mastodon.social/users/Edent
$follower_host = parse_url( $follower_actor, PHP_URL_HOST ); $follower_host = parse_url( $follower_actor, PHP_URL_HOST ); // E.g. mastodon.social
$follower_path = parse_url( $follower_actor, PHP_URL_PATH ); $follower_path = parse_url( $follower_actor, PHP_URL_PATH ); // E.g. /users/Edent
// Get the actor's profile as JSON // Get the actor's profile as JSON
// Is the actor an https URl? // Is the actor an https URl?
@ -305,7 +313,7 @@
// Check for errors // Check for errors
if (curl_errno($ch)) { if (curl_errno($ch)) {
// Handle cURL error // TODO: Handle cURL error
die(); die();
} }
@ -345,7 +353,7 @@
// The Accept is POSTed to the inbox on the server of the user who requested the follow // 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 ); $follower_inbox_path = parse_url( $follower_inbox, PHP_URL_PATH );
// Get the signed headers // Generate the signed headers
$headers = generate_signed_headers( $message, $follower_host, $follower_inbox_path, "POST" ); $headers = generate_signed_headers( $message, $follower_host, $follower_inbox_path, "POST" );
// POST the message and header to the requester's inbox // POST the message and header to the requester's inbox
@ -430,7 +438,7 @@
"Accept: application/activity+json", "Accept: application/activity+json",
); );
} else if ( "GET" == $method ) { } else if ( "GET" == $method ) {
// Sign the path, host, date - NO DIGEST // Sign the path, host, date - NO DIGEST because there's no message sent.
$stringToSign = "(request-target): get $path\nhost: $host\ndate: $date"; $stringToSign = "(request-target): get $path\nhost: $host\ndate: $date";
// The signing function returns the variable $signature // The signing function returns the variable $signature
@ -459,7 +467,7 @@
return $headers; return $headers;
} }
// User Interface for Homepage: // User Interface for Homepage.
// This creates a basic HTML page. This content appears when someone visits the root of your site. // This creates a basic HTML page. This content appears when someone visits the root of your site.
function home() { function home() {
global $username, $server, $realName, $summary, $directories; global $username, $server, $realName, $summary, $directories;
@ -491,10 +499,15 @@ HTML;
// Loop through the posts // Loop through the posts
foreach ($posts as $post) { foreach ($posts as $post) {
// Get the contents of the file
$postJSON = file_get_contents($post); $postJSON = file_get_contents($post);
$postData = json_decode($postJSON, true); $postData = json_decode($postJSON, true);
// Set up some common variables
$postTime = $postData["published"]; $postTime = $postData["published"];
$postHTML = $postData["content"]; $postHTML = $postData["content"];
// If the message has an image attached, display it.
if ( isset($postData["attachment"]) ) { if ( isset($postData["attachment"]) ) {
$postImgUrl = $postData["attachment"]["url"]; $postImgUrl = $postData["attachment"]["url"];
$postImgAlt = $postData["attachment"]["name"]; $postImgAlt = $postData["attachment"]["name"];
@ -502,6 +515,7 @@ HTML;
} else { } else {
$postImg = ""; $postImg = "";
} }
// Display the post // Display the post
echo "<li><a href='/{$post}'><time datetime='{$postTime}'>$postTime</time></a><br>{$postHTML}<br>{$postImg}</li>"; echo "<li><a href='/{$post}'><time datetime='{$postTime}'>$postTime</time></a><br>{$postHTML}<br>{$postImg}</li>";
} }
@ -546,8 +560,8 @@ HTML;
// Send Endpoint: // Send Endpoint:
// This takes the submitted message and checks the password is correct. // This takes the submitted message and checks the password is correct.
// It reads all the followers' data in `data/followers` // It reads all the followers' data in `data/followers`.
// It constructs a list of shared inboxes and unique inboxes // It constructs a list of shared inboxes and unique inboxes.
// It sends the message to every server that is following this account. // It sends the message to every server that is following this account.
function send() { function send() {
global $password, $server, $username, $key_private, $directories; global $password, $server, $username, $key_private, $directories;
@ -572,12 +586,9 @@ HTML;
// Files are stored according to their hash // Files are stored according to their hash
// A hash of "abc123" is stored in "/images/abc123.jpg" // A hash of "abc123" is stored in "/images/abc123.jpg"
$sha1 = sha1_file( $image ); $sha1 = sha1_file( $image );
$image_path = "images"; $image_full_path = $directories["images"] . "/{$sha1}.{$image_ext}";
$image_full_path = "{$image_path}/{$sha1}.{$image_ext}";
// Move media to the correct location // Move media to the correct location
// Create a directory if it doesn't exist
if( ! is_dir( $image_path ) ) { mkdir( $image_path ); }
move_uploaded_file( $image, $image_full_path ); move_uploaded_file( $image, $image_full_path );
// Get the alt text // Get the alt text
@ -606,7 +617,7 @@ HTML;
$guid = uuid(); $guid = uuid();
// Construct the Note // Construct the Note
// contentMap is used to prevent unnecessary "translate this post" pop ups // `contentMap` is used to prevent unnecessary "translate this post" pop ups
// hardcoded to English // hardcoded to English
$note = [ $note = [
"@context" => array( "@context" => array(
@ -645,15 +656,16 @@ HTML;
file_put_contents( $directories["posts"] . "/{$guid}.json", print_r( $note_json, true ) ); file_put_contents( $directories["posts"] . "/{$guid}.json", print_r( $note_json, true ) );
// Read existing followers // Read existing followers
$followers = glob( "data/followers/*.json" ); $followers = glob( $directories["followers"] . "/*.json" );
// Get all the inboxes // Get all the inboxes
$inboxes = []; $inboxes = [];
foreach ( $followers as $follower ) { foreach ( $followers as $follower ) {
// Get the data about the follower
$follower_info = json_decode( file_get_contents( $follower ), true ); $follower_info = json_decode( file_get_contents( $follower ), true );
// Some servers have "Shared inboxes" // Some servers have "Shared inboxes"
// If you have lots of followers on a single server, you only need to send the message once // If you have lots of followers on a single server, you only need to send the message once.
if( isset( $follower_info["endpoints"]["sharedInbox"] ) ) { if( isset( $follower_info["endpoints"]["sharedInbox"] ) ) {
$sharedInbox = $follower_info["endpoints"]["sharedInbox"]; $sharedInbox = $follower_info["endpoints"]["sharedInbox"];
if ( !in_array( $sharedInbox, $inboxes ) ) { if ( !in_array( $sharedInbox, $inboxes ) ) {
@ -680,7 +692,7 @@ HTML;
$inbox_host = parse_url( $inbox, PHP_URL_HOST ); $inbox_host = parse_url( $inbox, PHP_URL_HOST );
$inbox_path = parse_url( $inbox, PHP_URL_PATH ); $inbox_path = parse_url( $inbox, PHP_URL_PATH );
// Get the signed headers // Generate the signed headers
$headers = generate_signed_headers( $message, $inbox_host, $inbox_path, "POST" ); $headers = generate_signed_headers( $message, $inbox_host, $inbox_path, "POST" );
// POST the message and header to the requester's inbox // POST the message and header to the requester's inbox
@ -716,7 +728,7 @@ HTML;
global $server; global $server;
// Convert any URls into hyperlinks // Convert any URls into hyperlinks
$link_pattern = '/\bhttps?:\/\/\S+/iu'; $link_pattern = '/\bhttps?:\/\/\S+/iu'; // Sloppy regex
$replacement = function ( $match ) { $replacement = function ( $match ) {
$url = htmlspecialchars( $match[0], ENT_QUOTES, "UTF-8" ); $url = htmlspecialchars( $match[0], ENT_QUOTES, "UTF-8" );
return "<a href=\"$url\">$url</a>"; return "<a href=\"$url\">$url</a>";
@ -741,6 +753,7 @@ HTML;
} }
// Add HTML links for hashtags into the text // Add HTML links for hashtags into the text
// Todo: Make these links do something.
$content = preg_replace( $content = preg_replace(
$hashtag_pattern, $hashtag_pattern,
" <a href='https://{$server}/tag/$1'>#$1</a>", " <a href='https://{$server}/tag/$1'>#$1</a>",
@ -772,8 +785,8 @@ HTML;
} }
// Construct HTML breaks from carriage returns and line breaks // Construct HTML breaks from carriage returns and line breaks
$linebreak_patterns = array("\r\n", "\r", "\n"); // Variations of line breaks found in raw text $linebreak_patterns = array( "\r\n", "\r", "\n" ); // Variations of line breaks found in raw text
$content = str_replace($linebreak_patterns, "<br/>", $content); $content = str_replace( $linebreak_patterns, "<br/>", $content );
// Construct the content // Construct the content
$content = "<p>{$content}</p>"; $content = "<p>{$content}</p>";
@ -858,14 +871,16 @@ HTML;
// Get the posted content // Get the posted content
$user = $_POST["user"]; $user = $_POST["user"];
// Split the user into username and server // Split the user (@user@example.com) into username and server
list( , $follow_name, $follow_server ) = explode( "@", $user ); list( , $follow_name, $follow_server ) = explode( "@", $user );
// Get the Webfinger // Get the Webfinger
// This request does not need to be signed.
$webfingerURl = "https://{$follow_server}/.well-known/webfinger?resource=acct:{$follow_name}@{$follow_server}"; $webfingerURl = "https://{$follow_server}/.well-known/webfinger?resource=acct:{$follow_name}@{$follow_server}";
$webfingerJSON = file_get_contents( $webfingerURl ); $webfingerJSON = file_get_contents( $webfingerURl );
$webfinger = json_decode( $webfingerJSON, true ); $webfinger = json_decode( $webfingerJSON, true );
// Get the link to the user
foreach( $webfinger["links"] as $link ) { foreach( $webfinger["links"] as $link ) {
if ( "self" == $link["rel"] ) { if ( "self" == $link["rel"] ) {
$profileURl = $link["href"]; $profileURl = $link["href"];
@ -959,8 +974,9 @@ HTML;
} }
// Validate the Digest // Validate the Digest
// It is the hash of the raw input string, in binary, encoded as base64 // It is the hash of the raw input string, in binary, encoded as base64.
$digestString = $headers["digest"]; $digestString = $headers["digest"];
// Usually in the form `SHA-256=Ofv56Jm9rlowLR9zTkfeMGLUG1JYQZj0up3aRPZgT0c=` // Usually in the form `SHA-256=Ofv56Jm9rlowLR9zTkfeMGLUG1JYQZj0up3aRPZgT0c=`
// The Base64 encoding may have multiple `=` at the end. So split this at the first `=` // The Base64 encoding may have multiple `=` at the end. So split this at the first `=`
$digestData = explode( "=", $digestString, 2 ); $digestData = explode( "=", $digestString, 2 );