From b26b0871c8edabf69a2185be17efb13498ebcf69 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Wed, 28 Dec 2022 17:02:50 +0500 Subject: [PATCH] open source Streetwriters.Messenger --- Notesnook.sln | 6 + README.md | 10 +- Streetwriters.Messenger/Helpers/SSEHelper.cs | 45 ++++++ Streetwriters.Messenger/Program.cs | 61 ++++++++ .../Properties/launchSettings.json | 31 ++++ .../Services/HeartbeatService.cs | 64 ++++++++ Streetwriters.Messenger/Startup.cs | 142 ++++++++++++++++++ .../Streetwriters.Messenger.csproj | 22 +++ .../appsettings.Development.json | 9 ++ 9 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 Streetwriters.Messenger/Helpers/SSEHelper.cs create mode 100644 Streetwriters.Messenger/Program.cs create mode 100644 Streetwriters.Messenger/Properties/launchSettings.json create mode 100644 Streetwriters.Messenger/Services/HeartbeatService.cs create mode 100644 Streetwriters.Messenger/Startup.cs create mode 100644 Streetwriters.Messenger/Streetwriters.Messenger.csproj create mode 100644 Streetwriters.Messenger/appsettings.Development.json diff --git a/Notesnook.sln b/Notesnook.sln index 3dbb2c0..c104961 100644 --- a/Notesnook.sln +++ b/Notesnook.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Common", "Str EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Data", "Streetwriters.Data\Streetwriters.Data.csproj", "{CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetwriters.Messenger", "Streetwriters.Messenger\Streetwriters.Messenger.csproj", "{BDA80415-6C8D-4481-AC31-E5B4D73E9629}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +32,9 @@ Global {CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}.Debug|Any CPU.Build.0 = Debug|Any CPU {CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}.Release|Any CPU.ActiveCfg = Release|Any CPU {CBBA4BD8-B348-4CF0-A72A-0D3DE0E6001D}.Release|Any CPU.Build.0 = Release|Any CPU + {BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDA80415-6C8D-4481-AC31-E5B4D73E9629}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/README.md b/README.md index df83081..91d378e 100644 --- a/README.md +++ b/README.md @@ -25,19 +25,25 @@ Once you are inside the `./notesnook-sync-server` directory, run: dotnet restore Notesnook.sln ``` -Then build the Notesnook.API project: +To build the `Notesnook.API` project: ```bash dotnet build Notesnook.API/Notesnook.API.csproj ``` +To build the `Streetwriters.Messenger` project: + +```bash +dotnet build Streetwriters.Messenger/Streetwriters.Messenger.csproj +``` + ## TODO Self-hosting **Note: Self-hosting the Notesnook Sync Server is not yet possible. We are working to enable full on-premise self hosting so stay tuned!** - [x] Open source the Sync server - [ ] Open source the Identity server -- [ ] Open source the SSE Messaging infrastructure +- [x] Open source the SSE Messaging infrastructure - [ ] Fully Dockerize all services - [ ] Publish on DockerHub - [ ] Write self hosting docs diff --git a/Streetwriters.Messenger/Helpers/SSEHelper.cs b/Streetwriters.Messenger/Helpers/SSEHelper.cs new file mode 100644 index 0000000..b438184 --- /dev/null +++ b/Streetwriters.Messenger/Helpers/SSEHelper.cs @@ -0,0 +1,45 @@ +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2022 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System.Linq; +using System.Threading.Tasks; +using Lib.AspNetCore.ServerSentEvents; +using System.Security.Claims; + +namespace Streetwriters.Messenger.Helpers +{ + public class SSEHelper + { + public static async Task SendEventToUserAsync(string data, IServerSentEventsService sseService, string userId, string originTokenId = null) + { + var clients = sseService.GetClients().Where(c => c.User.FindFirstValue("sub") == userId); + foreach (var client in clients) + { + if (originTokenId != null && client.User.FindFirstValue("jti") == originTokenId) continue; + if (!client.IsConnected) continue; + await client.SendEventAsync(data); + } + } + + public static async Task SendEventToAllUsersAsync(string data, IServerSentEventsService sseService) + { + await sseService.SendEventAsync(data); + } + } +} \ No newline at end of file diff --git a/Streetwriters.Messenger/Program.cs b/Streetwriters.Messenger/Program.cs new file mode 100644 index 0000000..972bf65 --- /dev/null +++ b/Streetwriters.Messenger/Program.cs @@ -0,0 +1,61 @@ +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2022 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Streetwriters.Common; + +namespace Streetwriters.Messenger +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup() + .UseKestrel((options) => + { + options.Limits.MaxRequestBodySize = long.MaxValue; +#if DEBUG + options.ListenAnyIP(int.Parse(Servers.MessengerServer.Port)); +#else + options.ListenAnyIP(443, listenerOptions => + { + listenerOptions.UseHttps(Servers.OriginSSLCertificate); + }); + options.ListenAnyIP(80); + options.Listen(IPAddress.Parse(Servers.MessengerServer.Hostname), int.Parse(Servers.MessengerServer.Port)); +#endif + }); + }); + } +} diff --git a/Streetwriters.Messenger/Properties/launchSettings.json b/Streetwriters.Messenger/Properties/launchSettings.json new file mode 100644 index 0000000..f6598eb --- /dev/null +++ b/Streetwriters.Messenger/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53323", + "sslPort": 44382 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Streetwriters.Messenger": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Streetwriters.Messenger/Services/HeartbeatService.cs b/Streetwriters.Messenger/Services/HeartbeatService.cs new file mode 100644 index 0000000..766b4f0 --- /dev/null +++ b/Streetwriters.Messenger/Services/HeartbeatService.cs @@ -0,0 +1,64 @@ +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2022 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Lib.AspNetCore.ServerSentEvents; +using Streetwriters.Messenger.Helpers; +using System.Text.Json; + +namespace Streetwriters.Messenger.Services +{ + internal class HeartbeatService : BackgroundService + { + #region Fields + private const string HEARTBEAT_MESSAGE_FORMAT = "Streetwriters Heartbeat ({0} UTC)"; + + private readonly IServerSentEventsService _serverSentEventsService; + #endregion + + #region Constructor + public HeartbeatService(IServerSentEventsService serverSentEventsService) + { + _serverSentEventsService = serverSentEventsService; + } + #endregion + + #region Methods + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var message = JsonSerializer.Serialize(new + { + type = "heartbeat", + data = JsonSerializer.Serialize(new + { + t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + }) + }); + await SSEHelper.SendEventToAllUsersAsync(message, _serverSentEventsService); + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + } + #endregion + } +} \ No newline at end of file diff --git a/Streetwriters.Messenger/Startup.cs b/Streetwriters.Messenger/Startup.cs new file mode 100644 index 0000000..cd58bcf --- /dev/null +++ b/Streetwriters.Messenger/Startup.cs @@ -0,0 +1,142 @@ +/* +This file is part of the Notesnook Sync Server project (https://notesnook.com/) + +Copyright (C) 2022 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the Affero GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +Affero GNU General Public License for more details. + +You should have received a copy of the Affero GNU General Public License +along with this program. If not, see . +*/ + +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Reactive.Subjects; +using System.Text.Json; +using Lib.AspNetCore.ServerSentEvents; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Streetwriters.Common; +using Streetwriters.Common.Extensions; +using Streetwriters.Common.Messages; +using Streetwriters.Messenger.Helpers; +using Streetwriters.Messenger.Services; +using WampSharp.AspNetCore.WebSockets.Server; +using WampSharp.Binding; +using WampSharp.V2; +using WampSharp.V2.Realm; + +namespace Streetwriters.Messenger +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + + services.AddControllers(); + + JwtSecurityTokenHandler.DefaultMapInboundClaims = false; + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); + + services.AddCors(); + services.AddDistributedMemoryCache(delegate (MemoryDistributedCacheOptions cacheOptions) + { + cacheOptions.SizeLimit = 262144000L; + }); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddOAuth2Introspection("introspection", options => + { + options.Authority = Servers.IdentityServer.ToString(); + options.ClientSecret = Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET"); + options.ClientId = "notesnook"; + options.SaveToken = true; + options.EnableCaching = true; + options.CacheDuration = TimeSpan.FromMinutes(30); + // TODO + options.DiscoveryPolicy.RequireHttps = false; + }); + + services.AddServerSentEvents(); + services.AddSingleton(); + services.AddResponseCompression(options => + { + options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "text/event-stream" }); + }); + services.AddHealthChecks(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (!env.IsDevelopment()) + { + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto + }); + } + + app.UseCors("notesnook"); + + app.UseRouting(); + + app.UseAuthorization(); + app.UseAuthentication(); + + var options = new ServerSentEventsOptions(); + options.Authorization = new ServerSentEventsAuthorization() + { + AuthenticationSchemes = "introspection" + }; + app.MapServerSentEvents("/sse", options); + + app.UseWamp(WampServers.MessengerServer, (realm, server) => + { + IServerSentEventsService service = app.ApplicationServices.GetRequiredService(); + realm.Subscribe(server.Topics.SendSSETopic, async (ev) => + { + var message = JsonSerializer.Serialize(ev.Message); + if (ev.SendToAll) + { + await SSEHelper.SendEventToAllUsersAsync(message, service); + } + else + { + await SSEHelper.SendEventToUserAsync(message, service, ev.UserId, ev.OriginTokenId); + } + }); + }); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/health"); + }); + } + } +} diff --git a/Streetwriters.Messenger/Streetwriters.Messenger.csproj b/Streetwriters.Messenger/Streetwriters.Messenger.csproj new file mode 100644 index 0000000..9cc1a65 --- /dev/null +++ b/Streetwriters.Messenger/Streetwriters.Messenger.csproj @@ -0,0 +1,22 @@ + + + + net7.0 + Streetwriters.Messenger.Program + 10.0 + linux-x64 + true + + + + + + + + + + + + + + diff --git a/Streetwriters.Messenger/appsettings.Development.json b/Streetwriters.Messenger/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/Streetwriters.Messenger/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}