2024-02-12 21:21:05 +00:00
< ? php
/*
* " This code is not a code of honour... no highly esteemed code is commemorated here... nothing valued is here. "
* " What is here is dangerous and repulsive to us. This message is a warning about danger. "
* This is a rudimentary , single - file , low complexity , minimum functionality , ActivityPub server .
* For educational purposes only .
* It produces an Actor who can be followed .
* It can send messages to followers .
* It saves logs about requests it receives and sends .
* It is NOT suitable for production use .
* This code is " licenced " under CRAPL v0 - https :// matt . might . net / articles / crapl /
* " Any appearance of design in the Program is purely coincidental and should not in any way be mistaken for evidence of thoughtful software construction. "
*/
// Set up the Actor's information
$username = rawurlencode ( " example " ); // Encoded as it is often used as part of a URl
$realName = " E. Xample. Jr. " ;
2024-02-13 13:30:36 +00:00
$summary = " Some text about the user. " ;
2024-02-12 21:21:05 +00:00
$server = $_SERVER [ 'SERVER_NAME' ]; // Domain name this is hosted on
// Generate locally or from https://cryptotools.net/rsagen
// Newlines must be replaced with "\n"
$key_private = " -----BEGIN RSA PRIVATE KEY----- \n ... \n -----END RSA PRIVATE KEY----- " ;
$key_public = " -----BEGIN PUBLIC KEY----- \n ... \n -----END PUBLIC KEY----- " ;
// Password for sending messages
$password = " P4ssW0rd " ;
// Get all headers and requests sent to this server
$headers = print_r ( getallheaders (), true );
2024-02-13 13:30:36 +00:00
$postData = print_r ( $_POST , true );
$getData = print_r ( $_GET , true );
$filesData = print_r ( $_FILES , true );
$body = json_decode ( file_get_contents ( " php://input " ), true );
$bodyData = print_r ( $input , true );
2024-02-12 21:21:05 +00:00
$requestData = print_r ( $_REQUEST , true );
2024-02-13 13:30:36 +00:00
$serverData = print_r ( $_SERVER , true );
2024-02-12 21:21:05 +00:00
2024-02-13 13:30:36 +00:00
// Get the type of request - used in the log filename
2024-02-12 21:21:05 +00:00
if ( isset ( $body [ " type " ] ) ) {
$type = " " . $body [ " type " ];
} else {
$type = " " ;
}
// Create a timestamp in ISO 8601 format for the filename
$timestamp = date ( 'c' );
// Filename for the log
$filename = " { $timestamp } { $type } .txt " ;
2024-02-13 13:30:36 +00:00
// Save headers and request data to the timestamped file in the logs directory
if ( ! is_dir ( " logs " ) ) { mkdir ( " logs " ); }
file_put_contents ( " logs/ { $filename } " ,
2024-02-12 21:21:05 +00:00
" Headers: \n $headers \n \n " .
2024-02-13 13:30:36 +00:00
" Body Data: \n $bodyData \n \n " .
2024-02-12 21:21:05 +00:00
" POST Data: \n $postData \n \n " .
" GET Data: \n $getData \n \n " .
" Files Data: \n $filesData \n \n " .
" Request Data: \n $requestData\n\n " .
" Server Data: \n $serverData \n \n "
);
// The .htaccess changes /whatever to /?path=whatever
// What path was requested?
$path = $_GET [ " path " ];
switch ( $path ) {
case " " :
2024-02-13 13:30:36 +00:00
die ();
2024-02-12 21:21:05 +00:00
case " .well-known/webfinger " :
webfinger ();
2024-02-13 13:30:36 +00:00
case rawurldecode ( $username ) :
2024-02-12 21:21:05 +00:00
username ();
case " following " :
following ();
case " followers " :
followers ();
case " inbox " :
inbox ();
case " write " :
write ();
case " send " :
send ();
default :
die ();
}
function webfinger () {
// Display the WebFinger JSON
global $username , $server ;
$webfinger = array (
" subject " => " acct: { $username } @ { $server } " ,
" links " => array (
array (
" rel " => " self " ,
" type " => " application/activity+json " ,
" href " => " https:// { $server } / { $username } "
)
)
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $webfinger );
die ();
}
function username () {
// Display the username JSON
global $username , $realName , $server , $key_public ;
$user = array (
" @context " => [
" https://www.w3.org/ns/activitystreams " ,
" https://w3id.org/security/v1 "
],
" id " => " https:// { $server } / { $username } " ,
" type " => " Person " ,
" following " => " https:// { $server } /following " ,
" followers " => " https:// { $server } /followers " ,
" inbox " => " https:// { $server } /inbox " ,
" preferredUsername " => rawurldecode ( $username ),
" name " => " { $realName } " ,
2024-02-13 13:30:36 +00:00
" summary " => " { $summary } " ,
2024-02-12 21:21:05 +00:00
" url " => " https:// { $server } " ,
" manuallyApprovesFollowers " => true ,
" discoverable " => true ,
" published " => " 2024-02-12T11:51:00Z " ,
" icon " => [
" type " => " Image " ,
" mediaType " => " image/png " ,
" url " => " https:// { $server } /icon.png "
],
" publicKey " => [
" id " => " https:// { $server } / { $username } #main-key " ,
" owner " => " https:// { $server } / { $username } " ,
" publicKeyPem " => $key_public
]
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/activity+json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $user );
die ();
}
function following () {
// Display the following JSON
global $server ;
$following = array (
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } /following " ,
" type " => " Collection " ,
" totalItems " => 0 ,
" items " => []
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/activity+json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $following );
die ();
}
function followers () {
// Display the followers JSON
global $server ;
$followers = array (
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } /followers " ,
" type " => " Collection " ,
" totalItems " => 0 ,
" items " => []
);
2024-02-13 13:30:36 +00:00
header ( " Content-Type: application/activity+json " );
2024-02-12 21:21:05 +00:00
echo json_encode ( $followers );
die ();
}
function inbox () {
// Respond to InBox requests
global $body , $server , $username , $key_private ;
// Get the message and type
$inbox_message = $body ;
$inbox_type = $inbox_message [ " type " ];
// This inbox only responds to follow requests
if ( " Follow " != $inbox_type ) { die (); }
// Get the parameters
$inbox_id = $inbox_message [ " id " ];
$inbox_actor = $inbox_message [ " actor " ];
2024-02-13 13:30:36 +00:00
$inbox_url = parse_url ( $inbox_actor , PHP_URL_SCHEME ) . " :// " . parse_url ( $inbox_actor , PHP_URL_HOST );
$inbox_host = parse_url ( $inbox_actor , PHP_URL_HOST );
2024-02-12 21:21:05 +00:00
// 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 );
} else {
$followers_json = array ();
}
// 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 ) );
// Response Message ID
// This isn't used for anything important so could just be a random number
$guid = uuid ();
// Create the Accept message
$message = [
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } / { $guid } " ,
" type " => " Accept " ,
" actor " => " https:// { $server } / { $username } " ,
" object " => [
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => $inbox_id ,
" type " => $inbox_type ,
" actor " => $inbox_actor ,
" object " => " https:// { $server } / { $username } " ,
]
];
$message_json = json_encode ( $message );
// 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 ;
2024-02-13 13:30:36 +00:00
$path = parse_url ( $inbox_actor , PHP_URL_PATH ) . " /inbox " ;
2024-02-12 21:21:05 +00:00
// Set up signing
$keyId = " https:// { $server } / { $username } #main-key " ;
// Generate signing variables
2024-02-13 13:30:36 +00:00
$hash = hash ( 'sha256' , $message_json , true );
$digest = base64_encode ( $hash );
$date = date ( 'D, d M Y H:i:s \G\M\T' );
2024-02-12 21:21:05 +00:00
2024-02-13 13:30:36 +00:00
$signer = openssl_get_privatekey ( $key_private );
2024-02-12 21:21:05 +00:00
$stringToSign = " (request-target): post $path\nhost : $host\ndate : $date\ndigest : SHA-256= $digest " ;
2024-02-13 13:30:36 +00:00
openssl_sign (
$stringToSign ,
$signature ,
$signer ,
OPENSSL_ALGO_SHA256
);
$signature_b64 = base64_encode ( $signature );
2024-02-12 21:21:05 +00:00
$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: { $header } " ,
" Content-Type: application/activity+json " ,
" Accept: application/activity+json " ,
);
// 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 );
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_CUSTOMREQUEST , " POST " );
curl_setopt ( $ch , CURLOPT_POSTFIELDS , $message_json );
curl_setopt ( $ch , CURLOPT_HTTPHEADER , $headers );
$response = curl_exec ( $ch );
// Check for errors
if ( curl_errno ( $ch ) ) {
file_put_contents ( " error.txt " , curl_error ( $ch ) );
}
curl_close ( $ch );
die ();
}
function uuid () {
// Date sortable UUID
2024-02-13 13:30:36 +00:00
return sprintf ( '%08x-%04x-%04x-%04x-%012x' ,
2024-02-12 21:21:05 +00:00
time (),
mt_rand ( 0 , 0xffff ),
mt_rand ( 0 , 0xffff ),
mt_rand ( 0 , 0x3fff ) | 0x8000 ,
mt_rand ( 0 , 0xffffffffffff )
);
}
function write () {
// Display an HTML form for the user to enter a message.
echo <<< HTML
<! DOCTYPE html >
< html lang = " en-GB " >
< head >
< meta charset = " UTF-8 " >
< title > Send Message </ title >
< style >
* { font - family : sans - serif ; font - size : 1.1 em ;}
</ style >
</ head >
< body >
< form action = " /send " method = " post " enctype = " multipart/form-data " >
< label for = " content " > Your message :</ label >< br >
< textarea id = " content " name = " content " rows = " 5 " cols = " 32 " ></ textarea >< br >
< label for = " password " > Password </ label >< br >
< input type = " password " name = " password " id = " password " size = " 32 " >< br >
< input type = " submit " value = " Post Message " >
</ form >
</ body >
</ html >
HTML ;
die ();
}
function send () {
global $password , $server , $username , $key_private ;
// Does the posted password match the stored password?
if ( $password != $_POST [ " password " ] ) { die (); }
// Get the posted content
$content = $_POST [ " content " ];
2024-02-13 13:30:36 +00:00
// Current time - ISO8601
$timestamp = date ( " c " );
2024-02-12 21:21:05 +00:00
// Outgoing Message ID
$guid = uuid ();
// Construct the Note
2024-02-13 13:30:36 +00:00
// contentMap is used to prevent unnecessary "translate this post" pop ups
// hardcoded to English
2024-02-12 21:21:05 +00:00
$note = [
" @context " => array (
" https://www.w3.org/ns/activitystreams "
),
" id " => " https:// { $server } /posts/ { $guid } .json " ,
" type " => " Note " ,
" published " => $timestamp ,
" attributedTo " => " https:// { $server } / { $username } " ,
" content " => $content ,
" contentMap " => [ " en " => $content ],
" to " => [ " https://www.w3.org/ns/activitystreams#Public " ]
];
// Construct the Message
$message = [
" @context " => " https://www.w3.org/ns/activitystreams " ,
" id " => " https:// { $server } /posts/ { $guid } .json " ,
" type " => " Create " ,
" actor " => " https:// { $server } / { $username } " ,
" to " => [
" https://www.w3.org/ns/activitystreams#Public "
],
" cc " => [
" https:// { $server } /followers "
],
" object " => $note
];
$message_json = json_encode ( $message );
// Create the context for the permalink
2024-02-13 13:30:36 +00:00
$note = [ " @context " => " https://www.w3.org/ns/activitystreams " , ... $note ];
2024-02-12 21:21:05 +00:00
// Save the permalink
$note_json = json_encode ( $note );
2024-02-13 13:30:36 +00:00
// Check for posts/ directory and create it
if ( ! is_dir ( " posts " ) ) { mkdir ( " posts " ); }
2024-02-12 21:21:05 +00:00
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 );
// Prepare to use the multiple cURL handle
$mh = curl_multi_init ();
// Loop through all the severs of the followers
// Each server needs its own cURL handle
// Each POST to an inbox needs to be signed separately
foreach ( $hosts as $host ) {
$path = '/inbox' ;
// Set up signing
$privateKey = $key_private ;
$keyId = " https:// { $server } / { $username } #main-key " ;
$hash = hash ( " sha256 " , $message_json , true );
$digest = base64_encode ( $hash );
2024-02-13 13:30:36 +00:00
$date = date ( 'D, d M Y H:i:s \G\M\T' );
2024-02-12 21:21:05 +00:00
$signer = openssl_get_privatekey ( $key_private );
$stringToSign = " (request-target): post $path\nhost : $host\ndate : $date\ndigest : SHA-256= $digest " ;
2024-02-13 13:30:36 +00:00
openssl_sign (
$stringToSign ,
$signature ,
$signer ,
OPENSSL_ALGO_SHA256
);
$signature_b64 = base64_encode ( $signature );
2024-02-12 21:21:05 +00:00
$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: { $header } " ,
" Content-Type: application/activity+json " ,
" Accept: application/activity+json " ,
);
// Specify the URL of the remote server
$remoteServerUrl = " https:// { $host } { $path } " ;
// POST the message and header to the requester's inbox
$ch = curl_init ( $remoteServerUrl );
curl_setopt ( $ch , CURLOPT_RETURNTRANSFER , true );
curl_setopt ( $ch , CURLOPT_CUSTOMREQUEST , " POST " );
curl_setopt ( $ch , CURLOPT_POSTFIELDS , $message_json );
curl_setopt ( $ch , CURLOPT_HTTPHEADER , $headers );
// Add the handle to the multi-handle
curl_multi_add_handle ( $mh , $ch );
}
// Execute the multi-handle
do {
$status = curl_multi_exec ( $mh , $active );
if ( $active ) {
curl_multi_select ( $mh );
}
} while ( $active && $status == CURLM_OK );
// Close the multi-handle
curl_multi_close ( $mh );
// Render the JSON so the user can see the POST has worked
header ( " Location: https:// { $server } /posts/ { $guid } .json " );
die ();
}
2024-02-13 13:30:36 +00:00
die ();
die ();
die ();