2026-10-01 10:57:01 +02:00
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Http.Features ;
using PrivaPub.Federation.Signing ;
using System.Globalization ;
using System.Security.Cryptography ;
using System.Text ;
using System.Text.Json.Nodes ;
namespace PrivaPub.Tests.Support
{
public sealed class RemoteActor
{
readonly RSA _key = RSA . Create ( 2048 );
2026-10-01 12:30:57 +02:00
public RemoteActor ( Peer peer , string name , string origin = default , string type = "Person" )
2026-10-01 10:57:01 +02:00
{
Name = $"{name}{Guid.NewGuid():N}" [.. 20 ];
Id = $"{origin ?? peer.A}/users/{Name}" ;
2026-10-01 12:30:57 +02:00
Type = type ;
2026-10-01 10:57:01 +02:00
peer . Serve ( $"/users/{Name}" , Document (). ToJsonString ());
}
2026-10-01 12:30:57 +02:00
public string Type { get ; }
2026-10-01 10:57:01 +02:00
public string Name { get ; }
public string Id { get ; }
public string KeyId => Id + "#main-key" ;
2026-10-01 11:34:35 +02:00
public string SharedInbox => Id . Split ( "/users/" )[ 0 ] + "/inbox" ;
2026-10-01 10:57:01 +02:00
public JsonObject Document () => new ()
{
["id"] = Id ,
2026-10-01 12:30:57 +02:00
["type"] = Type ,
2026-10-01 10:57:01 +02:00
["preferredUsername"] = Name ,
["inbox"] = Id + "/inbox" ,
["followers"] = Id + "/followers" ,
["publicKey"] = new JsonObject
{
["id"] = KeyId ,
["owner"] = Id ,
["publicKeyPem"] = _key . ExportSubjectPublicKeyInfoPem ()
}
};
2026-10-03 10:43:03 +02:00
public HttpRequestMessage SignedGet ( string path , string host = "privapub.test" )
2026-10-01 12:30:57 +02:00
{
var date = DateTimeOffset . UtcNow . ToString ( "r" , CultureInfo . InvariantCulture );
2026-10-03 10:43:03 +02:00
var request = new HttpRequestMessage ( HttpMethod . Get , path );
request . Headers . TryAddWithoutValidation ( "Date" , date );
request . Headers . TryAddWithoutValidation ( "Accept" , "application/activity+json" );
request . Headers . TryAddWithoutValidation ( "Signature" , Signature ( $"(request-target): get {path}\nhost: {host}\ndate: {date}" , "(request-target) host date" ));
return request ;
}
public HttpRequestMessage SignedPost ( string path , JsonNode activity , string host = "privapub.test" , string date = default )
{
var body = Encoding . UTF8 . GetBytes ( activity . ToJsonString ());
date ??= DateTimeOffset . UtcNow . ToString ( "r" , CultureInfo . InvariantCulture );
var digest = HttpSignatures . Digest ( body );
var request = new HttpRequestMessage ( HttpMethod . Post , path ) { Content = new ByteArrayContent ( body ) };
request . Content . Headers . TryAddWithoutValidation ( "Content-Type" , "application/activity+json" );
request . Headers . TryAddWithoutValidation ( "Date" , date );
request . Headers . TryAddWithoutValidation ( "Digest" , digest );
request . Headers . TryAddWithoutValidation ( "Signature" ,
Signature ( $"(request-target): post {path}\nhost: {host}\ndate: {date}\ndigest: {digest}" , "(request-target) host date digest" ));
return request ;
}
string Signature ( string signingString , string headers )
{
2026-10-01 12:30:57 +02:00
var signature = Convert . ToBase64String ( _key . SignData ( Encoding . UTF8 . GetBytes ( signingString ), HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ));
2026-10-03 10:43:03 +02:00
return $"keyId=\" { KeyId } \ ",algorithm=\"rsa-sha256\",headers=\"{headers}\",signature=\"{signature}\"" ;
}
public HttpRequest Get ( string host , string path )
{
var date = DateTimeOffset . UtcNow . ToString ( "r" , CultureInfo . InvariantCulture );
2026-10-01 12:30:57 +02:00
var context = new DefaultHttpContext ();
context . Request . Method = "GET" ;
context . Request . Host = new HostString ( host );
context . Request . Path = path ;
context . Features . Get < IHttpRequestFeature >(). RawTarget = path ;
context . Request . Headers [ "Date" ] = date ;
2026-10-03 10:43:03 +02:00
context . Request . Headers [ "Signature" ] = Signature ( $"(request-target): get {path}\nhost: {host}\ndate: {date}" , "(request-target) host date" );
2026-10-01 12:30:57 +02:00
return context . Request ;
}
2026-10-01 10:57:01 +02:00
public HttpRequest Post ( string host , string path , JsonNode activity )
{
var body = Encoding . UTF8 . GetBytes ( activity . ToJsonString ());
var date = DateTimeOffset . UtcNow . ToString ( "r" , CultureInfo . InvariantCulture );
var digest = HttpSignatures . Digest ( body );
var context = new DefaultHttpContext ();
context . Request . Method = "POST" ;
context . Request . Host = new HostString ( host );
context . Request . Path = path ;
context . Features . Get < IHttpRequestFeature >(). RawTarget = path ;
context . Request . Headers [ "Date" ] = date ;
context . Request . Headers [ "Digest" ] = digest ;
context . Request . Headers [ "Content-Type" ] = "application/activity+json" ;
2026-10-03 10:43:03 +02:00
context . Request . Headers [ "Signature" ] = Signature ( $"(request-target): post {path}\nhost: {host}\ndate: {date}\ndigest: {digest}" , "(request-target) host date digest" );
2026-10-01 10:57:01 +02:00
context . Request . Body = new MemoryStream ( body );
context . Request . ContentLength = body . Length ;
return context . Request ;
}
}
}