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"
+ }
+ }
+}