starting working on activity

This commit is contained in:
Nicolas Constant 2020-06-06 01:29:13 -04:00
parent e1ef37d504
commit 4875e71340
No known key found for this signature in database
GPG key ID: 1E9F677FB01A5688
17 changed files with 329 additions and 5 deletions

View file

@ -0,0 +1,30 @@
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
{
public class ApDeserializer
{
public static Activity ProcessActivity(string json)
{
var activity = JsonConvert.DeserializeObject<Activity>(json);
switch (activity.type)
{
case "Follow":
return JsonConvert.DeserializeObject<ActivityFollow>(json);
case "Undo":
var a = JsonConvert.DeserializeObject<ActivityUndo>(json);
if(a.apObject.type == "Follow")
return JsonConvert.DeserializeObject<ActivityUndoFollow>(json);
break;
}
return null;
}
private class Ac : Activity
{
[JsonProperty("object")]
public Activity apObject { get; set; }
}
}
}

View file

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup>

View file

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
{
public class Activity
{
[JsonProperty("@context")]
public string context { get; set; }
public string id { get; set; }
public string type { get; set; }
public string actor { get; set; }
//[JsonProperty("object")]
//public string apObject { get; set; }
}
}

View file

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
{
public class ActivityFollow : Activity
{
[JsonProperty("object")]
public string apObject { get; set; }
}
}

View file

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
{
public class ActivityUndo : Activity
{
[JsonProperty("object")]
public Activity apObject { get; set; }
}
}

View file

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
{
public class ActivityUndoFollow : Activity
{
[JsonProperty("object")]
public ActivityFollow apObject { get; set; }
}
}

View file

@ -1,12 +1,14 @@
using System;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
{
public class Actor
{
[JsonPropertyName("@context")]
public string[] context { get; set; } = new[] {"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"};
//[JsonPropertyName("@context")]
[JsonProperty("@context")]
public object[] context { get; set; } = new[] {"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" };
public string id { get; set; }
public string type { get; set; }
public string preferredUsername { get; set; }

View file

@ -0,0 +1,26 @@
using System.Net.Http;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using Newtonsoft.Json;
namespace BirdsiteLive.Domain
{
public interface IActivityPubService
{
Task<Actor> GetUser(string objectId);
}
public class ActivityPubService : IActivityPubService
{
public async Task<Actor> GetUser(string objectId)
{
using (var httpClient = new HttpClient())
{
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var result = await httpClient.GetAsync(objectId);
var content = await result.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Actor>(content);
}
}
}
}

View file

@ -1,24 +1,33 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Cryptography;
using BirdsiteLive.Twitter.Models;
using Tweetinvi.Core.Exceptions;
namespace BirdsiteLive.Domain
{
public interface IUserService
{
Actor GetUser(TwitterUser twitterUser);
Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity);
}
public class UserService : IUserService
{
private readonly ICryptoService _cryptoService;
private readonly IActivityPubService _activityPubService;
private readonly string _host;
#region Ctor
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService)
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService)
{
_cryptoService = cryptoService;
_activityPubService = activityPubService;
_host = $"https://{instanceSettings.Domain.Replace("https://",string.Empty).Replace("http://", string.Empty).TrimEnd('/')}";
}
#endregion
@ -53,5 +62,78 @@ namespace BirdsiteLive.Domain
};
return user;
}
public async Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity)
{
// Validate
if (!await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders)) return false;
// Save Follow in DB
// Send Accept Activity
throw new NotImplementedException();
}
private async Task<bool> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders)
{
var signatures = rawSig.Split(',');
var signature_header = new Dictionary<string, string>();
foreach (var signature in signatures)
{
var splitSig = signature.Replace("\"", string.Empty).Split('=');
signature_header.Add(splitSig[0], splitSig[1]);
}
signature_header["signature"] = signature_header["signature"] + "==";
var key_id = signature_header["keyId"];
var headers = signature_header["headers"];
var algorithm = signature_header["algorithm"];
var sig = Convert.FromBase64String(signature_header["signature"]);
var remoteUser = await _activityPubService.GetUser(actor);
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();
//var comparisonString = headers.Split(' ').Select(signed_header_name =>
//{
// if (signed_header_name == "(request-target)")
// return "(request-target): post /inbox";
// else
// return $"{signed_header_name}: {r.Headers[signed_header_name.ToUpperInvariant()]}";
//});
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);
//var signKey = ASN1.ToRSA(Convert.FromBase64String(toDecode));
//new RSACryptoServiceProvider(keyId.publicKey.publicKeyPem);
//Create a new instance of RSACryptoServiceProvider.
RSACryptoServiceProvider key = new RSACryptoServiceProvider();
//Get an instance of RSAParameters from ExportParameters function.
RSAParameters RSAKeyInfo = key.ExportParameters(false);
//Set RSAKeyInfo to the public key values.
RSAKeyInfo.Modulus = Convert.FromBase64String(toDecode);
key.ImportParameters(RSAKeyInfo);
var result = signKey.VerifyData(Encoding.UTF8.GetBytes(toSign.ToString()), sig, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return result;
}
}
}

View file

@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.Domain", "Bird
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.ActivityPub", "BirdsiteLive.ActivityPub\BirdsiteLive.ActivityPub.csproj", "{7463E1E2-9736-4A46-8507-010BDD8ECFBB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BirdsiteLive.ActivityPub.Tests", "Tests\BirdsiteLive.ActivityPub.Tests\BirdsiteLive.ActivityPub.Tests.csproj", "{1D713961-9926-41FF-8D6A-8A4B8D548484}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -55,6 +57,10 @@ Global
{7463E1E2-9736-4A46-8507-010BDD8ECFBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7463E1E2-9736-4A46-8507-010BDD8ECFBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7463E1E2-9736-4A46-8507-010BDD8ECFBB}.Release|Any CPU.Build.0 = Release|Any CPU
{1D713961-9926-41FF-8D6A-8A4B8D548484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D713961-9926-41FF-8D6A-8A4B8D548484}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D713961-9926-41FF-8D6A-8A4B8D548484}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D713961-9926-41FF-8D6A-8A4B8D548484}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -66,6 +72,7 @@ Global
{155D46A4-2D05-47F2-8FFC-0B7C412A7652} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
{D48450EE-D8BD-4228-9864-043AC88F7EE0} = {4FEAD6BC-3C8E-451A-8CA1-FF1AF47D26CC}
{7463E1E2-9736-4A46-8507-010BDD8ECFBB} = {4FEAD6BC-3C8E-451A-8CA1-FF1AF47D26CC}
{1D713961-9926-41FF-8D6A-8A4B8D548484} = {A32D3458-09D0-4E0A-BA4B-8C411B816B94}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {69E8DCAD-4C37-4010-858F-5F94E6FBABCE}

View file

@ -4,9 +4,13 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.Domain;
using BirdsiteLive.Twitter;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
namespace BirdsiteLive.Controllers
{
@ -34,7 +38,8 @@ namespace BirdsiteLive.Controllers
if (r.Contains("application/activity+json"))
{
var apUser = _userService.GetUser(user);
return Json(apUser);
var jsonApUser = JsonConvert.SerializeObject(apUser);
return Content(jsonApUser, "application/json");
}
return View(user);
@ -48,11 +53,27 @@ namespace BirdsiteLive.Controllers
using (var reader = new StreamReader(Request.Body))
{
var body = await reader.ReadToEndAsync();
var activity = ApDeserializer.ProcessActivity(body);
// Do something
switch (activity.type)
{
case "Follow":
var succeeded = await _userService.FollowRequestedAsync(r.Headers["Signature"].First(), r.Method, r.Path, r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow);
if (succeeded) return Ok();
else return Unauthorized();
break;
default:
return Ok();
}
}
return Ok();
}
private Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
{
return header.ToDictionary<KeyValuePair<string, StringValues>, string, string>(h => h.Key, h => h.Value);
}
}
}

View file

@ -0,0 +1,33 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub.Tests
{
//[TestClass]
//public class ActivityTests
//{
// [TestMethod]
// public void FollowDeserializationTest()
// {
// var json = "{ \"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"}";
// var data = JsonConvert.DeserializeObject<Activity>(json);
// Assert.AreEqual("https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe", data.id);
// Assert.AreEqual("Follow", data.type);
// Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject);
// }
// [TestMethod]
// public void UndoDeserializationTest()
// {
// var json =
// "{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.technology/users/testtest#follows/225982/undo\",\"type\":\"Undo\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":{\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"}}";
// var data = JsonConvert.DeserializeObject<Activity>(json);
// Assert.AreEqual("https://mastodon.technology/users/testtest#follows/225982/undo", data.id);
// Assert.AreEqual("Undo", data.type);
// Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject);
// }
//}
}

View file

@ -0,0 +1,21 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub.Tests
{
[TestClass]
public class ActorTests
{
[TestMethod]
public void Deserialize()
{
var json =
"{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://w3id.org/security/v1\",{\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"toot\":\"http://joinmastodon.org/ns#\",\"featured\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"alsoKnownAs\":{\"@id\":\"as:alsoKnownAs\",\"@type\":\"@id\"},\"movedTo\":{\"@id\":\"as:movedTo\",\"@type\":\"@id\"},\"schema\":\"http://schema.org#\",\"PropertyValue\":\"schema:PropertyValue\",\"value\":\"schema:value\",\"IdentityProof\":\"toot:IdentityProof\",\"discoverable\":\"toot:discoverable\"}],\"id\":\"https://mastodon.technology/users/testtest\",\"type\":\"Person\",\"following\":\"https://mastodon.technology/users/testtest/following\",\"followers\":\"https://mastodon.technology/users/testtest/followers\",\"inbox\":\"https://mastodon.technology/users/testtest/inbox\",\"outbox\":\"https://mastodon.technology/users/testtest/outbox\",\"featured\":\"https://mastodon.technology/users/testtest/collections/featured\",\"preferredUsername\":\"testtest\",\"name\":\"TESTEST\",\"summary\":\"\u003cp\u003etest \u003cbr /\u003edsqdq65d4sq56d456q4d8zd4q685d45qd4sqd2q1d5zq56d465qsd4q65sd21qsd23q1s5d64qsd8q465d4s5q1d6qsd35qs4dq6sd84q\u003c/p\u003e\",\"url\":\"https://mastodon.technology/@testtest\",\"manuallyApprovesFollowers\":false,\"discoverable\":false,\"publicKey\":{\"id\":\"https://mastodon.technology/users/testtest#main-key\",\"owner\":\"https://mastodon.technology/users/testtest\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm7BlbWI/UD/YJj288h/5\nFB0gXZj0BjYVaK28uzTvb4w6eMu4qpbE9NI0bFqrloXzL3z6PaOCL4Myz9uJYolE\nZ9uNVi2OeZmHigNEOT3hkJWzddtrhkg8MLXKPdOETjhVWV3n+na7QWDDIXP7Fuvi\n+osA5LOoqtD1rYs87xUcWQPLCtVHs928FXsCdLO11ofXiNrancSzY17nkuufjWO+\ndLtvz1kx4Mt2V4Fu+DHskQAzPKU2tzGBrtlVQrk+1R63psIuZYDB6e4i7L6/d1Xl\nIQGmBeJfyxiuNIlbfZIbJ3xPYBQaVAnRKtyGVEFMWwZCqMySwc2LBX+rxI20zJ0R\n7wIDAQAB\n-----END PUBLIC KEY-----\n\"},\"tag\":[],\"attachment\":[],\"endpoints\":{\"sharedInbox\":\"https://mastodon.technology/inbox\"}}";
var actor = JsonConvert.DeserializeObject<Actor>(json);
}
}
}

View file

@ -0,0 +1,35 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub.Tests
{
[TestClass]
public class ApDeserializerTests
{
[TestMethod]
public void FollowDeserializationTest()
{
var json = "{ \"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"}";
var data = ApDeserializer.ProcessActivity(json) as ActivityFollow;
Assert.AreEqual("https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe", data.id);
Assert.AreEqual("Follow", data.type);
Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject);
}
[TestMethod]
public void UndoDeserializationTest()
{
var json =
"{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mastodon.technology/users/testtest#follows/225982/undo\",\"type\":\"Undo\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":{\"id\":\"https://mastodon.technology/c94567cf-1fda-42ba-82fc-a0f82f63ccbe\",\"type\":\"Follow\",\"actor\":\"https://mastodon.technology/users/testtest\",\"object\":\"https://4a120ca2680e.ngrok.io/users/manu\"}}";
var data = ApDeserializer.ProcessActivity(json) as ActivityUndoFollow;
Assert.AreEqual("https://mastodon.technology/users/testtest#follows/225982/undo", data.id);
Assert.AreEqual("Undo", data.type);
Assert.AreEqual("Follow", data.apObject.type);
Assert.AreEqual("https://mastodon.technology/users/testtest", data.apObject.actor);
Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject.apObject);
}
}
}

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BirdsiteLive.ActivityPub\BirdsiteLive.ActivityPub.csproj" />
</ItemGroup>
</Project>