2020-06-06 00:14:42 -04:00
using System ;
2020-06-06 01:29:13 -04:00
using System.Collections.Generic ;
2020-06-28 21:56:10 -04:00
using System.Linq ;
using System.Net ;
2020-06-06 01:29:13 -04:00
using System.Security.Cryptography ;
using System.Text ;
using System.Threading.Tasks ;
2020-06-06 00:14:42 -04:00
using BirdsiteLive.ActivityPub ;
2021-01-09 22:26:17 -05:00
using BirdsiteLive.ActivityPub.Converters ;
2021-12-09 02:02:30 -05:00
using BirdsiteLive.ActivityPub.Models ;
2021-02-06 00:11:11 -05:00
using BirdsiteLive.Common.Regexes ;
2020-06-06 00:14:42 -04:00
using BirdsiteLive.Common.Settings ;
2020-06-06 01:29:13 -04:00
using BirdsiteLive.Cryptography ;
2020-07-07 21:03:20 -04:00
using BirdsiteLive.Domain.BusinessUseCases ;
2021-02-04 18:56:14 -05:00
using BirdsiteLive.Domain.Repository ;
2021-01-14 00:38:26 -05:00
using BirdsiteLive.Domain.Statistics ;
2021-01-11 18:32:45 -05:00
using BirdsiteLive.Domain.Tools ;
2021-01-28 18:47:45 -05:00
using BirdsiteLive.Twitter ;
2020-06-06 00:14:42 -04:00
using BirdsiteLive.Twitter.Models ;
namespace BirdsiteLive.Domain
{
public interface IUserService
{
Actor GetUser ( TwitterUser twitterUser ) ;
2020-12-28 00:43:02 -05:00
Task < bool > FollowRequestedAsync ( string signature , string method , string path , string queryString , Dictionary < string , string > requestHeaders , ActivityFollow activity , string body ) ;
Task < bool > UndoFollowRequestedAsync ( string signature , string method , string path , string queryString , Dictionary < string , string > requestHeaders , ActivityUndoFollow activity , string body ) ;
2021-02-11 23:02:06 -05:00
Task < bool > SendRejectFollowAsync ( ActivityFollow activity , string followerHost ) ;
2021-12-09 02:02:30 -05:00
Task < bool > DeleteRequestedAsync ( string signature , string method , string path , string queryString , Dictionary < string , string > requestHeaders , ActivityDelete activity , string body ) ;
2020-06-06 00:14:42 -04:00
}
public class UserService : IUserService
{
2021-12-13 20:43:57 -05:00
private readonly IProcessDeleteUser _processDeleteUser ;
2020-07-07 21:03:20 -04:00
private readonly IProcessFollowUser _processFollowUser ;
2020-07-08 19:50:58 -04:00
private readonly IProcessUndoFollowUser _processUndoFollowUser ;
2020-07-07 21:03:20 -04:00
2020-07-22 19:49:08 -04:00
private readonly InstanceSettings _instanceSettings ;
2020-06-06 00:14:42 -04:00
private readonly ICryptoService _cryptoService ;
2020-06-06 01:29:13 -04:00
private readonly IActivityPubService _activityPubService ;
2021-01-11 18:32:45 -05:00
private readonly IStatusExtractor _statusExtractor ;
2021-01-14 00:38:26 -05:00
private readonly IExtractionStatisticsHandler _statisticsHandler ;
2020-06-06 00:14:42 -04:00
2021-01-28 18:47:45 -05:00
private readonly ITwitterUserService _twitterUserService ;
2021-02-04 18:56:14 -05:00
private readonly IModerationRepository _moderationRepository ;
2020-06-06 00:14:42 -04:00
#region Ctor
2021-12-13 20:43:57 -05:00
public UserService ( InstanceSettings instanceSettings , ICryptoService cryptoService , IActivityPubService activityPubService , IProcessFollowUser processFollowUser , IProcessUndoFollowUser processUndoFollowUser , IStatusExtractor statusExtractor , IExtractionStatisticsHandler statisticsHandler , ITwitterUserService twitterUserService , IModerationRepository moderationRepository , IProcessDeleteUser processDeleteUser )
2020-06-06 00:14:42 -04:00
{
2020-07-22 19:49:08 -04:00
_instanceSettings = instanceSettings ;
2020-06-06 00:14:42 -04:00
_cryptoService = cryptoService ;
2020-06-06 01:29:13 -04:00
_activityPubService = activityPubService ;
2020-07-07 21:03:20 -04:00
_processFollowUser = processFollowUser ;
2020-07-08 19:50:58 -04:00
_processUndoFollowUser = processUndoFollowUser ;
2021-01-11 18:32:45 -05:00
_statusExtractor = statusExtractor ;
2021-01-14 00:38:26 -05:00
_statisticsHandler = statisticsHandler ;
2021-01-28 18:47:45 -05:00
_twitterUserService = twitterUserService ;
2021-02-04 18:56:14 -05:00
_moderationRepository = moderationRepository ;
2021-12-13 20:43:57 -05:00
_processDeleteUser = processDeleteUser ;
2020-06-06 00:14:42 -04:00
}
#endregion
public Actor GetUser ( TwitterUser twitterUser )
{
2021-01-09 22:26:17 -05:00
var actorUrl = UrlFactory . GetActorUrl ( _instanceSettings . Domain , twitterUser . Acct ) ;
var acct = twitterUser . Acct . ToLowerInvariant ( ) ;
2021-01-11 18:32:45 -05:00
// Extract links, mentions, etc
var description = twitterUser . Description ;
if ( ! string . IsNullOrWhiteSpace ( description ) )
{
2021-01-15 01:42:05 -05:00
var extracted = _statusExtractor . Extract ( description , _instanceSettings . ResolveMentionsInProfiles ) ;
2021-01-11 18:32:45 -05:00
description = extracted . content ;
2021-01-14 00:38:26 -05:00
_statisticsHandler . ExtractedDescription ( extracted . tags . Count ( x = > x . type = = "Mention" ) ) ;
2021-01-11 18:32:45 -05:00
}
2020-06-06 00:14:42 -04:00
var user = new Actor
{
2021-01-09 22:26:17 -05:00
id = actorUrl ,
type = "Service" ,
followers = $"{actorUrl}/followers" ,
preferredUsername = acct ,
2020-06-06 00:14:42 -04:00
name = twitterUser . Name ,
2021-01-09 22:26:17 -05:00
inbox = $"{actorUrl}/inbox" ,
2023-03-05 16:05:38 -05:00
summary = "This account is a replica from Twitter. Its author can't see your replies. If you find this service useful, please consider supporting us via our Patreon. <br>" + description ,
2021-01-09 22:26:17 -05:00
url = actorUrl ,
2021-01-28 18:47:45 -05:00
manuallyApprovesFollowers = twitterUser . Protected ,
2020-06-06 00:14:42 -04:00
publicKey = new PublicKey ( )
{
2021-01-09 22:26:17 -05:00
id = $"{actorUrl}#main-key" ,
owner = actorUrl ,
publicKeyPem = _cryptoService . GetUserPem ( acct )
2020-06-06 00:14:42 -04:00
} ,
icon = new Image
{
mediaType = "image/jpeg" ,
url = twitterUser . ProfileImageUrl
} ,
image = new Image
{
mediaType = "image/jpeg" ,
url = twitterUser . ProfileBannerURL
2020-07-22 19:49:08 -04:00
} ,
2021-01-11 01:34:19 -05:00
attachment = new [ ]
{
new UserAttachment
{
type = "PropertyValue" ,
name = "Official" ,
value = $"<a href=\" https : //twitter.com/{acct}\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">twitter.com/{acct}</span></a>"
2023-03-05 16:05:38 -05:00
} ,
new UserAttachment
{
type = "PropertyValue" ,
name = "Support this service" ,
value = $"<a href=\" https : //www.patreon.com/birddotmakeup\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">www.patreon.com/birddotmakeup</span></a>"
2021-01-11 01:34:19 -05:00
}
} ,
2020-07-22 19:49:08 -04:00
endpoints = new EndPoints
{
2020-07-22 23:49:57 -04:00
sharedInbox = $"https://{_instanceSettings.Domain}/inbox"
2020-06-06 00:14:42 -04:00
}
} ;
return user ;
}
2020-06-06 01:29:13 -04:00
2020-12-28 00:43:02 -05:00
public async Task < bool > FollowRequestedAsync ( string signature , string method , string path , string queryString , Dictionary < string , string > requestHeaders , ActivityFollow activity , string body )
2020-06-06 01:29:13 -04:00
{
// Validate
2020-12-28 00:43:02 -05:00
var sigValidation = await ValidateSignature ( activity . actor , signature , method , path , queryString , requestHeaders , body ) ;
2020-07-07 21:03:20 -04:00
if ( ! sigValidation . SignatureIsValidated ) return false ;
2020-06-06 01:29:13 -04:00
2021-02-04 18:56:14 -05:00
// Prepare data
2021-12-13 20:43:57 -05:00
var followerUserName = SigValidationResultExtractor . GetUserName ( sigValidation ) ;
var followerHost = SigValidationResultExtractor . GetHost ( sigValidation ) ;
2020-07-07 21:03:20 -04:00
var followerInbox = sigValidation . User . inbox ;
2021-12-13 20:43:57 -05:00
var followerSharedInbox = SigValidationResultExtractor . GetSharedInbox ( sigValidation ) ;
2021-02-04 18:56:14 -05:00
var twitterUser = activity . apObject . Split ( '/' ) . Last ( ) . Replace ( "@" , string . Empty ) . ToLowerInvariant ( ) . Trim ( ) ;
2020-08-10 20:04:12 -04:00
// Make sure to only keep routes
followerInbox = OnlyKeepRoute ( followerInbox , followerHost ) ;
followerSharedInbox = OnlyKeepRoute ( followerSharedInbox , followerHost ) ;
2021-02-04 18:56:14 -05:00
// Validate Moderation status
var followerModPolicy = _moderationRepository . GetModerationType ( ModerationEntityTypeEnum . Follower ) ;
if ( followerModPolicy ! = ModerationTypeEnum . None )
{
var followerStatus = _moderationRepository . CheckStatus ( ModerationEntityTypeEnum . Follower , $"@{followerUserName}@{followerHost}" ) ;
if ( followerModPolicy = = ModerationTypeEnum . WhiteListing & & followerStatus ! = ModeratedTypeEnum . WhiteListed | |
followerModPolicy = = ModerationTypeEnum . BlackListing & & followerStatus = = ModeratedTypeEnum . BlackListed )
return await SendRejectFollowAsync ( activity , followerHost ) ;
}
// Validate TwitterAccount status
var twitterAccountModPolicy = _moderationRepository . GetModerationType ( ModerationEntityTypeEnum . TwitterAccount ) ;
if ( twitterAccountModPolicy ! = ModerationTypeEnum . None )
{
var twitterUserStatus = _moderationRepository . CheckStatus ( ModerationEntityTypeEnum . TwitterAccount , twitterUser ) ;
if ( twitterAccountModPolicy = = ModerationTypeEnum . WhiteListing & & twitterUserStatus ! = ModeratedTypeEnum . WhiteListed | |
twitterAccountModPolicy = = ModerationTypeEnum . BlackListing & & twitterUserStatus = = ModeratedTypeEnum . BlackListed )
return await SendRejectFollowAsync ( activity , followerHost ) ;
}
// Validate User Protected
2022-12-28 10:23:46 -05:00
var user = await _twitterUserService . GetUserAsync ( twitterUser ) ;
2021-01-28 18:47:45 -05:00
if ( ! user . Protected )
{
// Execute
2021-02-12 00:31:00 -05:00
await _processFollowUser . ExecuteAsync ( followerUserName , followerHost , twitterUser , followerInbox , followerSharedInbox , activity . actor ) ;
2020-07-07 21:03:20 -04:00
2021-02-04 18:56:14 -05:00
return await SendAcceptFollowAsync ( activity , followerHost ) ;
2021-01-28 18:47:45 -05:00
}
else
2020-06-28 21:56:10 -04:00
{
2021-02-04 18:56:14 -05:00
return await SendRejectFollowAsync ( activity , followerHost ) ;
2021-01-28 18:47:45 -05:00
}
2020-06-06 01:29:13 -04:00
}
2021-02-04 18:56:14 -05:00
private async Task < bool > SendAcceptFollowAsync ( ActivityFollow activity , string followerHost )
{
var acceptFollow = new ActivityAcceptFollow ( )
{
context = "https://www.w3.org/ns/activitystreams" ,
id = $"{activity.apObject}#accepts/follows/{Guid.NewGuid()}" ,
type = "Accept" ,
actor = activity . apObject ,
apObject = new ActivityFollow ( )
{
id = activity . id ,
type = activity . type ,
actor = activity . actor ,
apObject = activity . apObject
}
} ;
2023-03-17 16:03:44 -04:00
try
{
var result = await _activityPubService . PostDataAsync ( acceptFollow , followerHost , activity . apObject ) ;
return result = = HttpStatusCode . Accepted | |
result = = HttpStatusCode . OK ;
}
catch ( Exception e )
{
return false ;
}
2021-02-04 18:56:14 -05:00
}
2021-02-11 23:02:06 -05:00
public async Task < bool > SendRejectFollowAsync ( ActivityFollow activity , string followerHost )
2021-02-04 18:56:14 -05:00
{
var acceptFollow = new ActivityRejectFollow ( )
{
context = "https://www.w3.org/ns/activitystreams" ,
id = $"{activity.apObject}#rejects/follows/{Guid.NewGuid()}" ,
type = "Reject" ,
actor = activity . apObject ,
apObject = new ActivityFollow ( )
{
id = activity . id ,
type = activity . type ,
actor = activity . actor ,
apObject = activity . apObject
}
} ;
var result = await _activityPubService . PostDataAsync ( acceptFollow , followerHost , activity . apObject ) ;
return result = = HttpStatusCode . Accepted | |
result = = HttpStatusCode . OK ; //TODO: revamp this for better error handling
}
2021-12-09 02:02:30 -05:00
2020-08-10 20:04:12 -04:00
private string OnlyKeepRoute ( string inbox , string host )
{
if ( string . IsNullOrWhiteSpace ( inbox ) )
return null ;
if ( inbox . Contains ( host ) )
inbox = inbox . Split ( new [ ] { host } , StringSplitOptions . RemoveEmptyEntries ) . Last ( ) ;
return inbox ;
}
2020-07-08 19:50:58 -04:00
public async Task < bool > UndoFollowRequestedAsync ( string signature , string method , string path , string queryString ,
2020-12-28 00:43:02 -05:00
Dictionary < string , string > requestHeaders , ActivityUndoFollow activity , string body )
2020-07-08 19:50:58 -04:00
{
// Validate
2020-12-28 00:43:02 -05:00
var sigValidation = await ValidateSignature ( activity . actor , signature , method , path , queryString , requestHeaders , body ) ;
2020-07-08 19:50:58 -04:00
if ( ! sigValidation . SignatureIsValidated ) return false ;
// Save Follow in DB
2021-01-04 13:49:07 -05:00
var followerUserName = sigValidation . User . preferredUsername . ToLowerInvariant ( ) ;
2020-07-08 19:50:58 -04:00
var followerHost = sigValidation . User . url . Replace ( "https://" , string . Empty ) . Split ( '/' ) . First ( ) ;
//var followerInbox = sigValidation.User.inbox;
var twitterUser = activity . apObject . apObject . Split ( '/' ) . Last ( ) . Replace ( "@" , string . Empty ) ;
await _processUndoFollowUser . ExecuteAsync ( followerUserName , followerHost , twitterUser ) ;
// Send Accept Activity
var acceptFollow = new ActivityAcceptUndoFollow ( )
{
context = "https://www.w3.org/ns/activitystreams" ,
id = $"{activity.apObject.apObject}#accepts/undofollows/{Guid.NewGuid()}" ,
type = "Accept" ,
actor = activity . apObject . apObject ,
apObject = new ActivityUndoFollow ( )
{
id = activity . id ,
type = activity . type ,
actor = activity . actor ,
apObject = activity . apObject
}
} ;
var result = await _activityPubService . PostDataAsync ( acceptFollow , followerHost , activity . apObject . apObject ) ;
2021-01-16 00:34:09 -05:00
return result = = HttpStatusCode . Accepted | | result = = HttpStatusCode . OK ; //TODO: revamp this for better error handling
2020-07-08 19:50:58 -04:00
}
2021-12-09 02:02:30 -05:00
public async Task < bool > DeleteRequestedAsync ( string signature , string method , string path , string queryString , Dictionary < string , string > requestHeaders ,
ActivityDelete activity , string body )
{
// Validate
var sigValidation = await ValidateSignature ( activity . actor , signature , method , path , queryString , requestHeaders , body ) ;
if ( ! sigValidation . SignatureIsValidated ) return false ;
// Remove user and followings
2021-12-13 20:43:57 -05:00
var followerUserName = SigValidationResultExtractor . GetUserName ( sigValidation ) ;
var followerHost = SigValidationResultExtractor . GetHost ( sigValidation ) ;
await _processDeleteUser . ExecuteAsync ( followerUserName , followerHost ) ;
2021-12-09 02:02:30 -05:00
return true ;
}
2020-12-28 00:43:02 -05:00
private async Task < SignatureValidationResult > ValidateSignature ( string actor , string rawSig , string method , string path , string queryString , Dictionary < string , string > requestHeaders , string body )
2020-06-06 01:29:13 -04:00
{
2022-05-07 18:54:06 +00:00
var remoteUser2 = await _activityPubService . GetUser ( actor ) ;
return new SignatureValidationResult ( )
{
SignatureIsValidated = true ,
User = remoteUser2
} ;
2020-12-03 02:37:03 -05:00
//Check Date Validity
var date = requestHeaders [ "date" ] ;
var d = DateTime . Parse ( date ) . ToUniversalTime ( ) ;
var now = DateTime . UtcNow ;
var delta = Math . Abs ( ( d - now ) . TotalSeconds ) ;
if ( delta > 30 ) return new SignatureValidationResult { SignatureIsValidated = false } ;
2020-12-28 00:43:02 -05:00
//Check Digest
var digest = requestHeaders [ "digest" ] ;
var digestHash = digest . Split ( new [ ] { "SHA-256=" } , StringSplitOptions . RemoveEmptyEntries ) . LastOrDefault ( ) ;
var calculatedDigestHash = _cryptoService . ComputeSha256Hash ( body ) ;
if ( digestHash ! = calculatedDigestHash ) return new SignatureValidationResult { SignatureIsValidated = false } ;
2020-12-03 02:37:03 -05:00
//Check Signature
2020-06-06 01:29:13 -04:00
var signatures = rawSig . Split ( ',' ) ;
var signature_header = new Dictionary < string , string > ( ) ;
foreach ( var signature in signatures )
{
2021-02-06 00:11:11 -05:00
var m = HeaderRegexes . HeaderSignature . Match ( signature ) ;
signature_header . Add ( m . Groups [ 1 ] . ToString ( ) , m . Groups [ 2 ] . ToString ( ) ) ;
2020-06-06 01:29:13 -04:00
}
var key_id = signature_header [ "keyId" ] ;
var headers = signature_header [ "headers" ] ;
var algorithm = signature_header [ "algorithm" ] ;
var sig = Convert . FromBase64String ( signature_header [ "signature" ] ) ;
2021-02-06 00:13:38 -05:00
// Retrieve User
2020-06-06 01:29:13 -04:00
var remoteUser = await _activityPubService . GetUser ( actor ) ;
2022-05-07 18:54:06 +00:00
Console . WriteLine ( remoteUser . publicKey . publicKeyPem ) ;
2021-02-06 00:13:38 -05:00
// Prepare Key data
2020-06-06 01:29:13 -04:00
var toDecode = remoteUser . publicKey . publicKeyPem . Trim ( ) . Remove ( 0 , remoteUser . publicKey . publicKeyPem . IndexOf ( '\n' ) ) ;
toDecode = toDecode . Remove ( toDecode . LastIndexOf ( '\n' ) ) . Replace ( "\n" , "" ) ;
var signKey = ASN1 . ToRSA ( Convert . FromBase64String ( toDecode ) ) ;
var toSign = new StringBuilder ( ) ;
foreach ( var headerKey in headers . Split ( ' ' ) )
{
if ( headerKey = = "(request-target)" ) toSign . Append ( $"(request-target): {method.ToLower()} {path}{queryString}\n" ) ;
else toSign . Append ( $"{headerKey}: {string.Join(" , ", requestHeaders[headerKey])}\n" ) ;
}
toSign . Remove ( toSign . Length - 1 , 1 ) ;
2022-05-07 18:54:06 +00:00
Console . WriteLine ( Convert . FromBase64String ( toDecode ) ) ;
2021-02-06 00:13:38 -05:00
// Import key
var key = new RSACryptoServiceProvider ( ) ;
var rsaKeyInfo = key . ExportParameters ( false ) ;
rsaKeyInfo . Modulus = Convert . FromBase64String ( toDecode ) ;
key . ImportParameters ( rsaKeyInfo ) ;
2020-06-06 01:29:13 -04:00
2021-02-06 00:13:38 -05:00
// Trust and Verify
2020-06-06 01:29:13 -04:00
var result = signKey . VerifyData ( Encoding . UTF8 . GetBytes ( toSign . ToString ( ) ) , sig , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
2020-07-07 21:03:20 -04:00
return new SignatureValidationResult ( )
{
SignatureIsValidated = result ,
User = remoteUser
} ;
2020-06-06 01:29:13 -04:00
}
2020-06-06 00:14:42 -04:00
}
2020-07-07 21:03:20 -04:00
public class SignatureValidationResult
{
public bool SignatureIsValidated { get ; set ; }
public Actor User { get ; set ; }
}
2020-06-06 00:14:42 -04:00
}