Skip to content

Piero Bosio Social Web Site Personale Logo Fediverso

Social Forum federato con il resto del mondo. Non contano le istanze, contano le persone

ActivityPub Server in a Single PHP File

General Discussion
2 2 6
  • ActivityPub Server in a Single PHP File

    https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/

    Any computer program can be designed to run from a single file if you architect it wrong enough!

    I wanted to create the simplest possible Fediverse server which can be used as an educational tool to show how ActivityPub / Mastodon works.

    The design goals were:

    • Upload a single PHP file to the server.
    • No databases or separate config files.
    • Single Actor (i.e. not multi-user).
    • Allow the Actor to be followed.
    • Post plain-text messages to followers.
    • Be roughly standards compliant.

    And those goals have all been met! Check it out on GitLab. I warn you though, it is the nadir of bad coding. There are no tests, bugger-all security, scalability isn't considered, and it is a mess. But it works.

    You can follow the test user @example@example.viii.fi

    Architecture

    Firstly, I've slightly cheated on my "single file" stipulation. There's an .htaccess file which turns example.com/whatever into example.com/index.php?path=whatever

    The index.php file then takes that path and does stuff. It also contains all the configuration variables which is very bad practice.

    Rather than using a database, it saves files to disk.

    Again, this is not suitable for any real world use. This is an educational tool to help explain the basics of posting messages to the Fediverse. It requires absolutely no dependencies. You do not need to spin up a dockerised hypervisor to manage your node bundles and re-compile everything to WASM. Just FTP the file up to prod and you're done.

    Walkthrough

    This is a quick ramble through the code. It is reasonably well documented, I hope.

    Preamble

    This is where you set up your account's name and bio. You also need to provide a public/private keypair. The posting page is protected with a password that also needs to be set here.

     PHP    //  Set up the Actor's information    $username = rawurlencode("example");    //  Encoded as it is often used as part of a URl    $realName = "E. Xample. Jr.";    $summary  = "Some text about the user.";    $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";

    Logging

    ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them in /logs/ as a datestamped text file.

     PHP    // Get all headers and requests sent to this server    $headers     = print_r( getallheaders(), true );    $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( $body,    true );    $requestData = print_r( $_REQUEST, true );    $serverData  = print_r( $_SERVER,  true );    //  Get the type of request - used in the log filename    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";    //  Save headers and request data to the timestamped file in the logs directory    if( ! is_dir( "logs" ) ) { mkdir( "logs"); }    file_put_contents( "logs/{$filename}",         "Headers:     \n$headers    \n\n" .        "Body Data:   \n$bodyData   \n\n" .        "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"    );

    Routing

    The .htaccess changes /whatever to /?path=whateverThis runs the function of the path requested.

     PHP    !empty( $_GET["path"] )  ? $path = $_GET["path"] : die();    switch ($path) {        case ".well-known/webfinger":            webfinger();        case rawurldecode( $username ):            username();        case "following":            following();        case "followers":            followers();        case "inbox":            inbox();        case "write":            write();        case "send":            send();        default:            die();    }

    WebFinger

    The WebFinger Protocol is used to identify accounts.It is requested with example.com/.well-known/webfinger?resource=acct:username@example.comThis server only has one user, so it ignores the query string and always returns the same details.

     PHP    function webfinger() {        global $username, $server;        $webfinger = array(            "subject" => "acct:{$username}@{$server}",              "links" => array(                array(                     "rel" => "self",                    "type" => "application/activity+json",                    "href" => "https://{$server}/{$username}"                )            )        );        header( "Content-Type: application/json" );        echo json_encode( $webfinger );        die();    }

    Username

    Requesting example.com/username returns a JSON document with the user's information.

     PHP    function username() {        global $username, $realName, $summary, $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}",                              "summary" => "{$summary}",                                  "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            ]        );        header( "Content-Type: application/activity+json" );        echo json_encode( $user );        die();    }

    Following & Followers

    These JSON documents show how many users are following / followers-of this account.The information here is self-attested. So you can lie and use any number you want.

     PHPfunction following() {        global $server;        $following = array(              "@context" => "https://www.w3.org/ns/activitystreams",                    "id" => "https://{$server}/following",                  "type" => "Collection",            "totalItems" => 0,                 "items" => []        );        header( "Content-Type: application/activity+json" );        echo json_encode( $following );        die();    }    function followers() {        global $server;        $followers = array(              "@context" => "https://www.w3.org/ns/activitystreams",                    "id" => "https://{$server}/followers",                  "type" => "Collection",            "totalItems" => 0,                 "items" => []        );        header( "Content-Type: application/activity+json" );        echo json_encode( $followers );        die();    }

    Inbox

    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.This code does not cryptographically validate the headers of the received message.The name of the remote user's server is saved to a file so that future messages can be delivered to it.An accept request is cryptographically signed and POST'd back to the remote server.

     PHP    function inbox() {        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"];        $inbox_host  = parse_url( $inbox_actor, PHP_URL_HOST );        //  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}",            ]        ];        //  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";        //  Get the signed headers        $headers = generate_signed_headers( $message, $host, $path );        //  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,     json_encode($message) );        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();    }

    UUID

    Every message sent should have a unique ID. This can be anything you like. Some servers use a random number.I prefer a date-sortable string.

     PHP    function uuid() {        return sprintf( "%08x-%04x-%04x-%04x-%012x",            time(),            mt_rand(0, 0xffff),            mt_rand(0, 0xffff),            mt_rand(0, 0x3fff) | 0x8000,            mt_rand(0, 0xffffffffffff)        );    }

    Signing Headers

    Every message that your server sends needs to be cryptographically signed with your Private Key.This is a complicated process. Please read "How to make friends and verify requests" for more information.

     PHP    function generate_signed_headers( $message, $host, $path ) {        global $server, $username, $key_private;        //  Encode the message 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 );        //  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",        );        return $headers;    }

    User Interface for Writing

    This creates a basic HTML form. Type in your message and your password. It then POSTs the data to the /send endpoint.

     PHP    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.1em;}        </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();    }

    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.

     PHP    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"];        //  Current time - ISO8601        $timestamp = date( "c" );        //  Outgoing Message ID        $guid = uuid();        //  Construct the Note        //  contentMap is used to prevent unnecessary "translate this post" pop ups        // hardcoded to English        $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        ];        //  Create the context for the permalink        $note = [ "@context" => "https://www.w3.org/ns/activitystreams", ...$note ];        //  Save the permalink        $note_json = json_encode( $note );        //  Check for posts/ directory and create it        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 );        //  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";            //  Get the signed headers            $headers = generate_signed_headers( $message, $host, $path );            // 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,     json_encode($message) );            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();    }

    Next Steps

    This is not intended to be used in production. Ever. But if you would like to contribute more simple examples of how the protocol works, please come and play on GitLab.

    You can follow the test user @example@example.viii.fi

  • ActivityPub Server in a Single PHP File

    https://shkspr.mobi/blog/2024/02/activitypub-server-in-a-single-file/

    Any computer program can be designed to run from a single file if you architect it wrong enough!

    I wanted to create the simplest possible Fediverse server which can be used as an educational tool to show how ActivityPub / Mastodon works.

    The design goals were:

    • Upload a single PHP file to the server.
    • No databases or separate config files.
    • Single Actor (i.e. not multi-user).
    • Allow the Actor to be followed.
    • Post plain-text messages to followers.
    • Be roughly standards compliant.

    And those goals have all been met! Check it out on GitLab. I warn you though, it is the nadir of bad coding. There are no tests, bugger-all security, scalability isn't considered, and it is a mess. But it works.

    You can follow the test user @example@example.viii.fi

    Architecture

    Firstly, I've slightly cheated on my "single file" stipulation. There's an .htaccess file which turns example.com/whatever into example.com/index.php?path=whatever

    The index.php file then takes that path and does stuff. It also contains all the configuration variables which is very bad practice.

    Rather than using a database, it saves files to disk.

    Again, this is not suitable for any real world use. This is an educational tool to help explain the basics of posting messages to the Fediverse. It requires absolutely no dependencies. You do not need to spin up a dockerised hypervisor to manage your node bundles and re-compile everything to WASM. Just FTP the file up to prod and you're done.

    Walkthrough

    This is a quick ramble through the code. It is reasonably well documented, I hope.

    Preamble

    This is where you set up your account's name and bio. You also need to provide a public/private keypair. The posting page is protected with a password that also needs to be set here.

     PHP    //  Set up the Actor's information    $username = rawurlencode("example");    //  Encoded as it is often used as part of a URl    $realName = "E. Xample. Jr.";    $summary  = "Some text about the user.";    $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";

    Logging

    ActivityPub is a "chatty" protocol. This takes all the requests your server receives and saves them in /logs/ as a datestamped text file.

     PHP    // Get all headers and requests sent to this server    $headers     = print_r( getallheaders(), true );    $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( $body,    true );    $requestData = print_r( $_REQUEST, true );    $serverData  = print_r( $_SERVER,  true );    //  Get the type of request - used in the log filename    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";    //  Save headers and request data to the timestamped file in the logs directory    if( ! is_dir( "logs" ) ) { mkdir( "logs"); }    file_put_contents( "logs/{$filename}",         "Headers:     \n$headers    \n\n" .        "Body Data:   \n$bodyData   \n\n" .        "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"    );

    Routing

    The .htaccess changes /whatever to /?path=whateverThis runs the function of the path requested.

     PHP    !empty( $_GET["path"] )  ? $path = $_GET["path"] : die();    switch ($path) {        case ".well-known/webfinger":            webfinger();        case rawurldecode( $username ):            username();        case "following":            following();        case "followers":            followers();        case "inbox":            inbox();        case "write":            write();        case "send":            send();        default:            die();    }

    WebFinger

    The WebFinger Protocol is used to identify accounts.It is requested with example.com/.well-known/webfinger?resource=acct:username@example.comThis server only has one user, so it ignores the query string and always returns the same details.

     PHP    function webfinger() {        global $username, $server;        $webfinger = array(            "subject" => "acct:{$username}@{$server}",              "links" => array(                array(                     "rel" => "self",                    "type" => "application/activity+json",                    "href" => "https://{$server}/{$username}"                )            )        );        header( "Content-Type: application/json" );        echo json_encode( $webfinger );        die();    }

    Username

    Requesting example.com/username returns a JSON document with the user's information.

     PHP    function username() {        global $username, $realName, $summary, $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}",                              "summary" => "{$summary}",                                  "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            ]        );        header( "Content-Type: application/activity+json" );        echo json_encode( $user );        die();    }

    Following & Followers

    These JSON documents show how many users are following / followers-of this account.The information here is self-attested. So you can lie and use any number you want.

     PHPfunction following() {        global $server;        $following = array(              "@context" => "https://www.w3.org/ns/activitystreams",                    "id" => "https://{$server}/following",                  "type" => "Collection",            "totalItems" => 0,                 "items" => []        );        header( "Content-Type: application/activity+json" );        echo json_encode( $following );        die();    }    function followers() {        global $server;        $followers = array(              "@context" => "https://www.w3.org/ns/activitystreams",                    "id" => "https://{$server}/followers",                  "type" => "Collection",            "totalItems" => 0,                 "items" => []        );        header( "Content-Type: application/activity+json" );        echo json_encode( $followers );        die();    }

    Inbox

    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.This code does not cryptographically validate the headers of the received message.The name of the remote user's server is saved to a file so that future messages can be delivered to it.An accept request is cryptographically signed and POST'd back to the remote server.

     PHP    function inbox() {        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"];        $inbox_host  = parse_url( $inbox_actor, PHP_URL_HOST );        //  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}",            ]        ];        //  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";        //  Get the signed headers        $headers = generate_signed_headers( $message, $host, $path );        //  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,     json_encode($message) );        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();    }

    UUID

    Every message sent should have a unique ID. This can be anything you like. Some servers use a random number.I prefer a date-sortable string.

     PHP    function uuid() {        return sprintf( "%08x-%04x-%04x-%04x-%012x",            time(),            mt_rand(0, 0xffff),            mt_rand(0, 0xffff),            mt_rand(0, 0x3fff) | 0x8000,            mt_rand(0, 0xffffffffffff)        );    }

    Signing Headers

    Every message that your server sends needs to be cryptographically signed with your Private Key.This is a complicated process. Please read "How to make friends and verify requests" for more information.

     PHP    function generate_signed_headers( $message, $host, $path ) {        global $server, $username, $key_private;        //  Encode the message 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 );        //  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",        );        return $headers;    }

    User Interface for Writing

    This creates a basic HTML form. Type in your message and your password. It then POSTs the data to the /send endpoint.

     PHP    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.1em;}        </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();    }

    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.

     PHP    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"];        //  Current time - ISO8601        $timestamp = date( "c" );        //  Outgoing Message ID        $guid = uuid();        //  Construct the Note        //  contentMap is used to prevent unnecessary "translate this post" pop ups        // hardcoded to English        $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        ];        //  Create the context for the permalink        $note = [ "@context" => "https://www.w3.org/ns/activitystreams", ...$note ];        //  Save the permalink        $note_json = json_encode( $note );        //  Check for posts/ directory and create it        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 );        //  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";            //  Get the signed headers            $headers = generate_signed_headers( $message, $host, $path );            // 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,     json_encode($message) );            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();    }

    Next Steps

    This is not intended to be used in production. Ever. But if you would like to contribute more simple examples of how the protocol works, please come and play on GitLab.

    You can follow the test user @example@example.viii.fi

    @blog@shkspr.mobi oh.. my god.. i love it


Gli ultimi otto messaggi ricevuti dalla Federazione
Post suggeriti
  • 0 Votes
    1 Posts
    8 Views
    Destroying Autocracy – November 27, 2025Welcome to this week’s “Destroying Autocracy”.It’s your source for curated news affecting democracy in the cyber arena with a focus on protecting it. That necessitates an opinionated Butlerian jihad against big tech as well as evangelizing for open-source and the Fediverse. Since big media’s journalism wing is flailing and failing in its core duty to democracy, this is also a collection of alternative reporting on the eternal battle between autocracy and democracy. We also cover the cybersecurity world. You can’t be free without safety and privacy.FYI, my opinions will be in bold. And will often involve cursing. Because humans. Especially tech bros. And fascists. Fuck ’em.The Programmer’s Fulcrum is the future (and smaller) home for a fusion of Symfony Station and Battalion. Its tagline is Devs Defending Democracy, Developing the OMN.You can sign up now and for 2025 get an email with links to each week’s Symfony Station Communiqué and Battalion “Destroying Autocracy” post along with their featured articles. And you’ll be set with TPF after the fusing in January.We are posting on the Fediverse now at @thefulcrum @thefulcrum.dev and original website content will start in 2026.Featured Item(s)The Atlantic writes:Over the weekend, Elon Musk’s X rolled out a feature that had the immediate result of sowing maximum chaos. The update, called “About This Account,” allows people to click on the profile of an X user and see such information as: which country the account was created in, where its user is currently based, and how many times the username has been changed.Nikita Bier, X’s head of product, said the feature was “an important first step to securing the integrity of the global town square.” Roughly four hours later, with the update in the wild, Bier sent another post: “I need a drink.”Almost immediately, “About This Account” stated that many prominent and prolific pro-MAGA accounts, which signaled that they were run by “patriotic” Americans, were based in countries such as Nigeria, Russia, India, and Thailand.@MAGANationX, an account with almost 400,000 followers and whose bio says it is a “Patriot Voice for We The People,” is based in “Eastern Europe (Non-EU),” according to the feature, and has changed its username five times since the account was made, last year.On X and Bluesky, users dredged up countless examples of fake or misleading rage-baiting accounts posting aggressive culture-war takes to large audiences. An account called “Maga Nadine” claims to be living in and posting from the United States but is, according to X, based in Morocco. An “America First” account with 67,000 followers is apparently based in Bangladesh. Poetically, the X handle @American is based in Pakistan, according to the feature.Elon Musk’s Worthless, Poisoned Hall of MirrorsJust FYI, December 25th will be the day I stop exploring the stupidity of our current timeline and the last Destroying Autocracy post. Again, see the notes above about The Fulcrum.We start and end with good news to make the middle bearable.The response to Russia’s War Crimes, Techno Feudalism, and other douchebaggeryEuroNews reports:Life after chatbots: Meet the ‘AI vegans’ refusing to accept a virtual realityKagi is:Introducing SlopStop: Community-driven AI slop detection in Kagi SearchOrion 1.0 ✴︎ Browse BeyondFucking awesome if you use Macs.TechPolicy Press reports:Why Civil Society Is Sounding the Alarm on the EU’s Omnibus RollbackThe Guardian reports:European parliament calls for social media ban on under-16sThe Free Software Foundation Europe shares:Germany Stack: Only Free Software Enables Digital SovereigntyWikimedia announces:Unifying our mobile and desktop domainsDecidim reviews:Decidim Fest 2025: Collective energy, digital sovereignty and a common roadmapThe Conversation reports:Tim Berners-Lee wants everyone to own their own data – his plan needs state and consumer support to workMullvad reports:An important victory – but we still need to stop Chat Control.Ploum says:Don’t Do Snake Oil WritingFiona Fokus says:I don’t care how well your “AI” worksAbso-fucking-lutely.NeutralThe Guardian asks:Has Britain become an economic colony?England always makes sure to ape the shitty parts of America.W3C shares:Preventing Abuse of Digital CredentialsBen Werdmuller covers:The EFF we need nowThe Evil Empire (AKA Autocracy) Strikes BackHeise reports:Analysis of the Digital Sovereignty Summit: Open Source Gets ScoldedWe Are Solomon reports:Hungry for data: Inside Europol’s secretive AI programThe Intercept reports:The FBI wants to use Surveillance Drones with Facial Recognition TechnologyHow Corporate Partnerships Powered University Surveillance of Palestine ProtestsThe Counter Offensive reports:Witkoff was secretly giving Russians advicePariah StatesThe Register reports:CISA warns spyware crews are breaking into Signal and WhatsApp accountsDarkReading reports:DPRK’s FlexibleFerret Tightens macOS GripBig MediaThe Columbia Journalism Review reports:Could Public Skepticism of the Press Actually Be Good for Democracy?It could since most Big Media is owned by right-wing c^nts.ProPublica shares:ProPublica’s May-August 2025 Impact Report: Independent Investigations That Spur ChangeBig TechSage JournalsAlgorithms at your service: Understanding how X’s systems of recommendation likely fueled the far-right riots in the United Kingdom by amplifying visual representations of racist conspiracy theoriesRenée DiResta reports:On the internet, nobody knows you’re a MAGA influencer… in LagosThe Daily Beast reports:Top MAGA Influencers Accidentally Unmasked as Foreign Trolls404 Media reports:America’s Polarization Has Become the World’s Side HustleThe Register reports:Meta knows how bad its sites are for kids, say lawyersMM+M reports:What healthcare marketers need to know about Meta’s censoring of abortion adsThe Markup reports:How American Big Tech guards the profits it extracts around the worldCybersecurity/PrivacyBleepingComputer reports:Cox Enterprises discloses Oracle E-Business Suite data breachHmm, cable companies are literally shit at everything.Code beautifiers expose credentials from banks, govt, tech orgsDarkReading reports:Infamous Shai-hulud Worm Resurfaces From the DepthsThe Register reports:FCC guts post-Salt Typhoon telco rules despite ongoing espionage riskFediverseTerence Eden has:Now witness the power of this fully operational Fediverse!Ghost has:Explore the independent webRadWeb Hosting shares:How to Host Your Own Mastodon Server on a VPS (5 Minute Quick-Start Guide)How to Install Pleroma on Ubuntu VPS (5 Minute Quick-Start Guide)NodeBB announces:NodeBB v4.7.0 — category boost fixes, remote media/emoji in chats, and more!Slightly Decentralized Social MediaConnected Places has:ATmosphere Report – 144CTAs (aka show us some free love)That’s it for this week. Please share this edition of Destroying Autocracy.Follow me on the Fediverse. Or this site via the button in the footer. Or via RSS. Or even our future home in 2026, if you want a head start.Keep fighting!Ringleader, BattalionReuben Walker Follow me on the Fediverse#activitypub #ai #atproto #autocracy #bigJournalism #bigTech #democracy #fascism #fediverse #mastodon #nodebb #pleroma #stopChina #stopIsrael #stopRedAmerica #stopRussia #supportUkraine #technoanarchism #technofeudalismhttps://battalion.mobileatom.net/?p=3992
  • 🥳 PHP 8.5 Released!

    Uncategorized php php85 release
    1
    0 Votes
    1 Posts
    9 Views
    🥳 PHP 8.5 Released!In this new release, we have:🌐 URI Extension▶️ Pipe Operator📑 Clone With⚠️ A New #[\NoDiscard] Attribute🆕 Closures and First-Class Callables in Constant Expressions🌀 Persistent cURL Share Handles👓 Read all about it on: https://www.php.net/releases/8.5/🔗 https://php.net/ChangeLog-8#8.5.0📦 https://php.net/downloads#PHP #PHP85 #Release
  • 0 Votes
    1 Posts
    12 Views
    Video: Lunchtime Timelapse 1-26-17Andy's Video Pub | original post: https://video.andyrush.net/video/lunchtime-timelapse-1-26-17/#ActivityPub #Fediverse #ReclaimOpen25 #unf
  • 0 Votes
    4 Posts
    15 Views
    @giacomo @informapirata @beachcomber @alephoto85 Snac2 is great. The only thing that got me back at hosting a full mastodon instance was because I wanted to use a mastodon mobile app fully. So it depend how you intend to use it :)But like I just said; a mastodon app. Snac2 is not Mastodon. It has, howerver, a "compatibility" mastodon API. It was just not enough for me personally.But beside that; Snac2 is really a game changer. Stable, reliable and low on memory/cpu usage.