diff --git a/src/BirdsiteLive.ActivityPub/ApDeserializer.cs b/src/BirdsiteLive.ActivityPub/ApDeserializer.cs index ef978d9..1eaffa8 100644 --- a/src/BirdsiteLive.ActivityPub/ApDeserializer.cs +++ b/src/BirdsiteLive.ActivityPub/ApDeserializer.cs @@ -16,6 +16,31 @@ namespace BirdsiteLive.ActivityPub if(a.apObject.type == "Follow") return JsonConvert.DeserializeObject(json); break; + case "Accept": + var accept = JsonConvert.DeserializeObject(json); + //var acceptType = JsonConvert.DeserializeObject(accept.apObject); + switch ((accept.apObject as dynamic).type.ToString()) + { + case "Follow": + var acceptFollow = new ActivityAcceptFollow() + { + type = accept.type, + id = accept.id, + actor = accept.actor, + context = accept.context, + apObject = new ActivityFollow() + { + id = (accept.apObject as dynamic).id?.ToString(), + type = (accept.apObject as dynamic).type?.ToString(), + actor = (accept.apObject as dynamic).actor?.ToString(), + context = (accept.apObject as dynamic).context?.ToString(), + apObject = (accept.apObject as dynamic).@object?.ToString() + } + }; + return acceptFollow; + break; + } + break; } return null; diff --git a/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj b/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj index 01a891a..8dfebd7 100644 --- a/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj +++ b/src/BirdsiteLive.ActivityPub/BirdsiteLive.ActivityPub.csproj @@ -5,6 +5,7 @@ + diff --git a/src/BirdsiteLive.ActivityPub/Models/Activity.cs b/src/BirdsiteLive.ActivityPub/Models/Activity.cs index 8a93505..0ad51f2 100644 --- a/src/BirdsiteLive.ActivityPub/Models/Activity.cs +++ b/src/BirdsiteLive.ActivityPub/Models/Activity.cs @@ -10,6 +10,7 @@ namespace BirdsiteLive.ActivityPub public string id { get; set; } public string type { get; set; } public string actor { get; set; } + //[JsonProperty("object")] //public string apObject { get; set; } } diff --git a/src/BirdsiteLive.ActivityPub/Models/ActivityAccept.cs b/src/BirdsiteLive.ActivityPub/Models/ActivityAccept.cs new file mode 100644 index 0000000..30b8746 --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Models/ActivityAccept.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace BirdsiteLive.ActivityPub +{ + public class ActivityAccept : Activity + { + [JsonProperty("object")] + public object apObject { get; set; } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptFollow.cs b/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptFollow.cs new file mode 100644 index 0000000..f833a43 --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Models/ActivityAcceptFollow.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace BirdsiteLive.ActivityPub +{ + public class ActivityAcceptFollow : Activity + { + [JsonProperty("object")] + public ActivityFollow apObject { get; set; } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.ActivityPub/Models/ActivityCreate.cs b/src/BirdsiteLive.ActivityPub/Models/ActivityCreate.cs new file mode 100644 index 0000000..8532682 --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Models/ActivityCreate.cs @@ -0,0 +1,7 @@ +namespace BirdsiteLive.ActivityPub +{ + public class ActivityCreate + { + + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.ActivityPub/Models/ActivityCreateNote.cs b/src/BirdsiteLive.ActivityPub/Models/ActivityCreateNote.cs new file mode 100644 index 0000000..d100b3a --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Models/ActivityCreateNote.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace BirdsiteLive.ActivityPub +{ + public class ActivityCreateNote : Activity + { + public string published { get; set; } + public string[] to { get; set; } + public string[] cc { get; set; } + + [JsonProperty("object")] + public Note apObject { get; set; } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.ActivityPub/Models/Note.cs b/src/BirdsiteLive.ActivityPub/Models/Note.cs new file mode 100644 index 0000000..0dc9c16 --- /dev/null +++ b/src/BirdsiteLive.ActivityPub/Models/Note.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace BirdsiteLive.ActivityPub +{ + public class Note + { + public string id { get; set; } + public string type { get; } = "Note"; + public string summary { get; set; } + public string inReplyTo { get; set; } + public string published { get; set; } + public string url { get; set; } + public string attributedTo { get; set; } + public string[] to { get; set; } + public string[] cc { get; set; } + public bool sensitive { get; set; } + //public string conversation { get; set; } + public string content { get; set; } + //public Dictionary contentMap { get; set; } + public string[] attachment { get; set; } + public string[] tag { get; set; } + //public Dictionary replies; + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Domain/ActivityPubService.cs b/src/BirdsiteLive.Domain/ActivityPubService.cs index e80877c..d30daf8 100644 --- a/src/BirdsiteLive.Domain/ActivityPubService.cs +++ b/src/BirdsiteLive.Domain/ActivityPubService.cs @@ -1,17 +1,31 @@ -using System.Net.Http; +using System; +using System.Net; +using System.Net.Http; +using System.Text; using System.Threading.Tasks; using BirdsiteLive.ActivityPub; using Newtonsoft.Json; +using Org.BouncyCastle.Bcpg; namespace BirdsiteLive.Domain { public interface IActivityPubService { Task GetUser(string objectId); + Task PostDataAsync(T data, string targetHost, string actorUrl, string inbox = null); } public class ActivityPubService : IActivityPubService { + private readonly ICryptoService _cryptoService; + + #region Ctor + public ActivityPubService(ICryptoService cryptoService) + { + _cryptoService = cryptoService; + } + #endregion + public async Task GetUser(string objectId) { using (var httpClient = new HttpClient()) @@ -22,5 +36,37 @@ namespace BirdsiteLive.Domain return JsonConvert.DeserializeObject(content); } } + + public async Task PostDataAsync(T data, string targetHost, string actorUrl, string inbox = null) + { + var usedInbox = $"/inbox"; + if (!string.IsNullOrWhiteSpace(inbox)) + usedInbox = inbox; + + var json = JsonConvert.SerializeObject(data); + + var date = DateTime.UtcNow.ToUniversalTime(); + var httpDate = date.ToString("r"); + var signature = _cryptoService.SignAndGetSignatureHeader(date, actorUrl, targetHost, usedInbox); + + + + var client = new HttpClient(); + var httpRequestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"https://{targetHost}/{usedInbox}"), + Headers = + { + {"Host", targetHost}, + {"Date", httpDate}, + {"Signature", signature} + }, + Content = new StringContent(json, Encoding.UTF8, "application/ld+json") + }; + + var response = await client.SendAsync(httpRequestMessage); + return response.StatusCode; + } } } \ No newline at end of file diff --git a/src/BirdsiteLive.Domain/CryptoService.cs b/src/BirdsiteLive.Domain/CryptoService.cs index 151344c..ed62a59 100644 --- a/src/BirdsiteLive.Domain/CryptoService.cs +++ b/src/BirdsiteLive.Domain/CryptoService.cs @@ -1,10 +1,13 @@ -using BirdsiteLive.Domain.Factories; +using System; +using System.Text; +using BirdsiteLive.Domain.Factories; namespace BirdsiteLive.Domain { public interface ICryptoService { string GetUserPem(string id); + string SignAndGetSignatureHeader(DateTime date, string actor, string host, string inbox = null); } public class CryptoService : ICryptoService @@ -22,5 +25,29 @@ namespace BirdsiteLive.Domain { return _magicKeyFactory.GetMagicKey().AsPEM; } + + /// + /// + /// + /// + /// in the form of https://domain.io/actor + /// in the form of domain.io + /// + public string SignAndGetSignatureHeader(DateTime date, string actor, string targethost, string inbox = null) + { + var usedInbox = "/inbox"; + if (!string.IsNullOrWhiteSpace(inbox)) + usedInbox = inbox; + + var httpDate = date.ToString("r"); + + var signedString = $"(request-target): post {usedInbox}\nhost: {targethost}\ndate: {httpDate}"; + var signedStringBytes = Encoding.UTF8.GetBytes(signedString); + var signature = _magicKeyFactory.GetMagicKey().Sign(signedStringBytes); + var sig64 = Convert.ToBase64String(signature); + + var header = "keyId=\"" + actor + "\",headers=\"(request-target) host date\",signature=\"" + sig64 + "\""; + return header; + } } } \ No newline at end of file diff --git a/src/BirdsiteLive.Domain/UserService.cs b/src/BirdsiteLive.Domain/UserService.cs index 174fbab..5c569f5 100644 --- a/src/BirdsiteLive.Domain/UserService.cs +++ b/src/BirdsiteLive.Domain/UserService.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -69,11 +71,25 @@ namespace BirdsiteLive.Domain if (!await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders)) return false; // Save Follow in DB - - // Send Accept Activity - - throw new NotImplementedException(); + // Send Accept Activity + var targetHost = activity.actor.Replace("https://", string.Empty).Split('/').First(); + 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 + } + }; + var result = await _activityPubService.PostDataAsync(acceptFollow, targetHost, activity.apObject); + return result == HttpStatusCode.Accepted; } private async Task ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary requestHeaders) diff --git a/src/BirdsiteLive/BirdsiteLive.csproj b/src/BirdsiteLive/BirdsiteLive.csproj index d302ae7..332831e 100644 --- a/src/BirdsiteLive/BirdsiteLive.csproj +++ b/src/BirdsiteLive/BirdsiteLive.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/BirdsiteLive/Controllers/DebugController.cs b/src/BirdsiteLive/Controllers/DebugController.cs new file mode 100644 index 0000000..f187d32 --- /dev/null +++ b/src/BirdsiteLive/Controllers/DebugController.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using BirdsiteLive.ActivityPub; +using BirdsiteLive.Common.Settings; +using BirdsiteLive.Domain; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Newtonsoft.Json; + +namespace BirdsiteLive.Controllers +{ + public class DebugController : Controller + { + private readonly InstanceSettings _instanceSettings; + private readonly ICryptoService _cryptoService; + private readonly IActivityPubService _activityPubService; + + #region Ctor + public DebugController(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService) + { + _instanceSettings = instanceSettings; + _cryptoService = cryptoService; + _activityPubService = activityPubService; + } + #endregion + + public IActionResult Index() + { + return View(); + } + + [HttpPost] + public async Task Follow() + { + var actor = $"https://{_instanceSettings.Domain}/users/gra"; + var targethost = "mastodon.technology"; + var followActivity = new ActivityFollow() + { + context = "https://www.w3.org/ns/activitystreams", + id = $"https://{_instanceSettings.Domain}/{Guid.NewGuid()}", + type = "Follow", + actor = actor, + apObject = $"https://{targethost}/users/testtest" + }; + + await _activityPubService.PostDataAsync(followActivity, targethost, actor); + + return View("Index"); + } + + [HttpPost] + public async Task PostNote() + { + var username = "gra"; + var actor = $"https://{_instanceSettings.Domain}/users/{username}"; + var targetHost = "mastodon.technology"; + var target = $"{targetHost}/users/testtest"; + var inbox = $"/users/testtest/inbox"; + + var noteGuid = Guid.NewGuid(); + var noteId = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{noteGuid}"; + var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteGuid}"; + + var to = $"{actor}/followers"; + var apPublic = "https://www.w3.org/ns/activitystreams#Public"; + + var now = DateTime.UtcNow; + var nowString = now.ToString("s") + "Z"; + + var noteActivity = new ActivityCreateNote() + { + context = "https://www.w3.org/ns/activitystreams", + id = $"{noteId}/activity", + type = "Create", + actor = actor, + published = nowString, + to = new []{ to }, + //cc = new [] { apPublic }, + apObject = new Note() + { + id = noteId, + summary = null, + inReplyTo = null, + published = nowString, + url = noteUrl, + attributedTo = actor, + to = new[] { to }, + //cc = new [] { apPublic }, + sensitive = false, + content = "

Woooot

", + attachment = new string[0], + tag = new string[0] + } + }; + + await _activityPubService.PostDataAsync(noteActivity, targetHost, actor, inbox); + + return View("Index"); + } + } + + public static class HtmlHelperExtensions + { + public static bool IsDebug() + { +#if DEBUG + return true; +#else + return false; +#endif + } + } +} \ No newline at end of file diff --git a/src/BirdsiteLive/Controllers/InboxController.cs b/src/BirdsiteLive/Controllers/InboxController.cs index 301bf6e..82de7b3 100644 --- a/src/BirdsiteLive/Controllers/InboxController.cs +++ b/src/BirdsiteLive/Controllers/InboxController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -14,6 +15,15 @@ namespace BirdsiteLive.Controllers [HttpPost] public async Task Inbox() { + var r = Request; + using (var reader = new StreamReader(Request.Body)) + { + var body = await reader.ReadToEndAsync(); + + + } + + throw new NotImplementedException(); } } diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index 91c8d69..05a6aac 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -60,11 +60,13 @@ namespace BirdsiteLive.Controllers { 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(); + if (succeeded) return Accepted(); else return Unauthorized(); break; + case "Undo": + return Accepted(); default: - return Ok(); + return Accepted(); } } diff --git a/src/BirdsiteLive/Views/Home/Index.cshtml b/src/BirdsiteLive/Views/Home/Index.cshtml index d2d19bd..6ae1367 100644 --- a/src/BirdsiteLive/Views/Home/Index.cshtml +++ b/src/BirdsiteLive/Views/Home/Index.cshtml @@ -1,8 +1,15 @@ -@{ +@using BirdsiteLive.Controllers; +@{ ViewData["Title"] = "Home Page"; }

Welcome

Learn about building Web apps with ASP.NET Core.

+ + + @if (HtmlHelperExtensions.IsDebug()) + { + Debug + }
diff --git a/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs b/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs index ab4441c..3c85113 100644 --- a/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs +++ b/src/Tests/BirdsiteLive.ActivityPub.Tests/ApDeserializerTests.cs @@ -31,5 +31,30 @@ namespace BirdsiteLive.ActivityPub.Tests Assert.AreEqual("https://mastodon.technology/users/testtest", data.apObject.actor); Assert.AreEqual("https://4a120ca2680e.ngrok.io/users/manu", data.apObject.apObject); } + + [TestMethod] + public void AcceptDeserializationTest() + { + var json = "{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"https://mamot.fr/users/testtest#accepts/follows/333879\",\"type\":\"Accept\",\"actor\":\"https://mamot.fr/users/testtest\",\"object\":{\"id\":\"https://85da1577f778.ngrok.io/f89dfd87-f5ce-4603-83d9-405c0e229989\",\"type\":\"Follow\",\"actor\":\"https://85da1577f778.ngrok.io/users/gra\",\"object\":\"https://mamot.fr/users/testtest\"}}"; + + + var data = ApDeserializer.ProcessActivity(json) as ActivityAcceptFollow; + Assert.AreEqual("https://mamot.fr/users/testtest#accepts/follows/333879", data.id); + Assert.AreEqual("Accept", data.type); + Assert.AreEqual("https://mamot.fr/users/testtest", data.actor); + Assert.AreEqual("https://85da1577f778.ngrok.io/f89dfd87-f5ce-4603-83d9-405c0e229989", data.apObject.id); + Assert.AreEqual("https://85da1577f778.ngrok.io/users/gra", data.apObject.actor); + Assert.AreEqual("Follow", data.apObject.type); + Assert.AreEqual("https://mamot.fr/users/testtest", data.apObject.apObject); + } + + //[TestMethod] + //public void NoteDeserializationTest() + //{ + // var json = + // "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",{\"ostatus\":\"http://ostatus.org#\",\"atomUri\":\"ostatus:atomUri\",\"inReplyToAtomUri\":\"ostatus:inReplyToAtomUri\",\"conversation\":\"ostatus:conversation\",\"sensitive\":\"as:sensitive\",\"toot\":\"http://joinmastodon.org/ns#\",\"votersCount\":\"toot:votersCount\"}],\"id\":\"https://mastodon.technology/users/testtest/statuses/104424839893177182/activity\",\"type\":\"Create\",\"actor\":\"https://mastodon.technology/users/testtest\",\"published\":\"2020-06-29T02:10:04Z\",\"to\":[\"https://mastodon.technology/users/testtest/followers\"],\"cc\":[],\"object\":{\"id\":\"https://mastodon.technology/users/testtest/statuses/104424839893177182\",\"type\":\"Note\",\"summary\":null,\"inReplyTo\":null,\"published\":\"2020-06-29T02:10:04Z\",\"url\":\"https://mastodon.technology/@testtest/104424839893177182\",\"attributedTo\":\"https://mastodon.technology/users/testtest\",\"to\":[\"https://mastodon.technology/users/testtest/followers\"],\"cc\":[],\"sensitive\":false,\"atomUri\":\"https://mastodon.technology/users/testtest/statuses/104424839893177182\",\"inReplyToAtomUri\":null,\"conversation\":\"tag:mastodon.technology,2020-06-29:objectId=34900058:objectType=Conversation\",\"content\":\"

test

\",\"contentMap\":{\"en\":\"

test

\"},\"attachment\":[],\"tag\":[],\"replies\":{\"id\":\"https://mastodon.technology/users/testtest/statuses/104424839893177182/replies\",\"type\":\"Collection\",\"first\":{\"type\":\"CollectionPage\",\"next\":\"https://mastodon.technology/users/testtest/statuses/104424839893177182/replies?only_other_accounts=true&page=true\",\"partOf\":\"https://mastodon.technology/users/testtest/statuses/104424839893177182/replies\",\"items\":[]}}}}"; + + // var data = ApDeserializer.ProcessActivity(json) as ActivityAcceptFollow; + //} } } \ No newline at end of file diff --git a/src/Tests/BirdsiteLive.Cryptography.Tests/RsaKeysTests.cs b/src/Tests/BirdsiteLive.Cryptography.Tests/RsaKeysTests.cs index 50c77b9..2e4ae5e 100644 --- a/src/Tests/BirdsiteLive.Cryptography.Tests/RsaKeysTests.cs +++ b/src/Tests/BirdsiteLive.Cryptography.Tests/RsaKeysTests.cs @@ -1,34 +1,34 @@ -using System.Security.Cryptography; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MyProject.Data.Encryption; +//using System.Security.Cryptography; +//using Microsoft.VisualStudio.TestTools.UnitTesting; +//using MyProject.Data.Encryption; -namespace BirdsiteLive.Cryptography.Tests -{ - [TestClass] - public class RsaKeysTests - { - [TestMethod] - public void TestMethod1() - { - var rsa = RSA.Create(); +//namespace BirdsiteLive.Cryptography.Tests +//{ +// [TestClass] +// public class RsaKeysTests +// { +// [TestMethod] +// public void TestMethod1() +// { +// var rsa = RSA.Create(); - var cspParams = new CspParameters(); - cspParams.ProviderType = 1; // PROV_RSA_FULL - cspParams.Flags = CspProviderFlags.CreateEphemeralKey; - var rsaProvider = new RSACryptoServiceProvider(2048, cspParams); +// var cspParams = new CspParameters(); +// cspParams.ProviderType = 1; // PROV_RSA_FULL +// cspParams.Flags = CspProviderFlags.CreateEphemeralKey; +// var rsaProvider = new RSACryptoServiceProvider(2048, cspParams); - var rsaPublicKey = RSAKeys.ExportPublicKey(rsaProvider); - var rsaPrivateKey = RSAKeys.ExportPrivateKey(rsaProvider); +// var rsaPublicKey = RSAKeys.ExportPublicKey(rsaProvider); +// var rsaPrivateKey = RSAKeys.ExportPrivateKey(rsaProvider); - //rsaProvider. +// //rsaProvider. - var pem = RSAKeys.ImportPublicKey(rsaPrivateKey); - } +// var pem = RSAKeys.ImportPublicKey(rsaPrivateKey); +// } - [TestMethod] - public void TestMethod2() - { +// [TestMethod] +// public void TestMethod2() +// { - } - } -} \ No newline at end of file +// } +// } +//} \ No newline at end of file