diff --git a/MareAPI b/MareAPI new file mode 160000 index 0000000..7b1d82a --- /dev/null +++ b/MareAPI @@ -0,0 +1 @@ +Subproject commit 7b1d82ac1c66dbe66f84e1146bfba3c753ee0e55 diff --git a/MareSynchronosServer/MareSynchronosAuthService/Startup.cs b/MareSynchronosServer/MareSynchronosAuthService/Startup.cs new file mode 100644 index 0000000..5e6c401 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Startup.cs @@ -0,0 +1,263 @@ +using MareSynchronosAuthService.Controllers; +using MareSynchronosShared.Metrics; +using MareSynchronosShared.Services; +using MareSynchronosShared.Utils; +using Microsoft.AspNetCore.Mvc.Controllers; +using StackExchange.Redis.Extensions.Core.Configuration; +using StackExchange.Redis.Extensions.System.Text.Json; +using StackExchange.Redis; +using System.Net; +using MareSynchronosAuthService.Services; +using MareSynchronosShared.RequirementHandlers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using MareSynchronosShared.Data; +using Microsoft.EntityFrameworkCore; +using Prometheus; +using Microsoft.AspNetCore.HttpOverrides; +using MareSynchronosShared.Utils.Configuration; + +namespace MareSynchronosAuthService; + +public class Startup +{ + private readonly IConfiguration _configuration; + private ILogger _logger; + + public Startup(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) + { + var config = app.ApplicationServices.GetRequiredService>(); + + // Respect X-Forwarded-* headers from the reverse proxy so generated links use the public scheme/host + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor + }); + + app.UseRouting(); + + app.UseHttpMetrics(); + + app.UseAuthentication(); + app.UseAuthorization(); + + KestrelMetricServer metricServer = new KestrelMetricServer(config.GetValueOrDefault(nameof(MareConfigurationBase.MetricsPort), 4985)); + metricServer.Start(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapHealthChecks("/healthz").WithMetadata(new AllowAnonymousAttribute()); + + foreach (var source in endpoints.DataSources.SelectMany(e => e.Endpoints).Cast()) + { + if (source == null) continue; + _logger.LogInformation("Endpoint: {url} ", source.RoutePattern.RawText); + } + }); + } + + public void ConfigureServices(IServiceCollection services) + { + var mareConfig = _configuration.GetRequiredSection("MareSynchronos"); + + services.AddHttpContextAccessor(); + + ConfigureRedis(services, mareConfig); + + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + + services.AddHostedService(provider => provider.GetRequiredService()); + + services.Configure(_configuration.GetRequiredSection("MareSynchronos")); + services.Configure(_configuration.GetRequiredSection("MareSynchronos")); + + services.AddSingleton(); + // Nearby discovery services (well-known + presence) + services.AddSingleton(); + services.AddHostedService(p => p.GetRequiredService()); + + // Presence store selection + var discoveryStore = _configuration.GetValue("NearbyDiscovery:Store") ?? "memory"; + TimeSpan presenceTtl = TimeSpan.FromMinutes(_configuration.GetValue("NearbyDiscovery:PresenceTtlMinutes", 5)); + TimeSpan tokenTtl = TimeSpan.FromSeconds(_configuration.GetValue("NearbyDiscovery:TokenTtlSeconds", 120)); + if (string.Equals(discoveryStore, "redis", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var mux = sp.GetRequiredService(); + return new MareSynchronosAuthService.Services.Discovery.RedisPresenceStore(logger, mux, presenceTtl, tokenTtl); + }); + } + else + { + services.AddSingleton(sp => new MareSynchronosAuthService.Services.Discovery.InMemoryPresenceStore(presenceTtl, tokenTtl)); + } + + services.AddSingleton(); + services.AddHostedService(p => p.GetRequiredService()); + + ConfigureAuthorization(services); + + ConfigureDatabase(services, mareConfig); + + ConfigureConfigServices(services); + + ConfigureMetrics(services); + + services.AddHealthChecks(); + services.AddControllers().ConfigureApplicationPartManager(a => + { + a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController), typeof(WellKnownController), typeof(DiscoveryController))); + }); + + services.AddSingleton(); + services.AddHostedService(p => p.GetRequiredService()); + } + + private static void ConfigureAuthorization(IServiceCollection services) + { + services.AddTransient(); + + services.AddOptions(JwtBearerDefaults.AuthenticationScheme) + .Configure>((options, config) => + { + options.TokenValidationParameters = new() + { + ValidateIssuer = false, + ValidateLifetime = true, + ValidateAudience = false, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue(nameof(MareConfigurationBase.Jwt)))), + }; + }); + + services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(); + + services.AddAuthorization(options => + { + options.DefaultPolicy = new AuthorizationPolicyBuilder() + .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) + .RequireAuthenticatedUser().Build(); + options.AddPolicy("Authenticated", policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + policy.RequireAuthenticatedUser(); + }); + options.AddPolicy("Identified", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified)); + + }); + options.AddPolicy("Admin", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Administrator)); + + }); + options.AddPolicy("Moderator", policy => + { + policy.AddRequirements(new UserRequirement(UserRequirements.Identified | UserRequirements.Moderator | UserRequirements.Administrator)); + }); + options.AddPolicy("Internal", new AuthorizationPolicyBuilder().RequireClaim(MareClaimTypes.Internal, "true").Build()); + }); + } + + private static void ConfigureMetrics(IServiceCollection services) + { + services.AddSingleton(m => new MareMetrics(m.GetService>(), new List + { + MetricsAPI.CounterAuthenticationCacheHits, + MetricsAPI.CounterAuthenticationFailures, + MetricsAPI.CounterAuthenticationRequests, + MetricsAPI.CounterAuthenticationSuccesses, + MetricsAPI.CounterAccountsCreated, + }, new List + { + })); + } + + private static void ConfigureRedis(IServiceCollection services, IConfigurationSection mareConfig) + { + // configure redis for SignalR + var redisConnection = mareConfig.GetValue(nameof(ServerConfiguration.RedisConnectionString), string.Empty); + + var options = ConfigurationOptions.Parse(redisConnection); + + var endpoint = options.EndPoints[0]; + string address = ""; + int port = 0; + if (endpoint is DnsEndPoint dnsEndPoint) { address = dnsEndPoint.Host; port = dnsEndPoint.Port; } + if (endpoint is IPEndPoint ipEndPoint) { address = ipEndPoint.Address.ToString(); port = ipEndPoint.Port; } + var redisConfiguration = new RedisConfiguration() + { + AbortOnConnectFail = true, + KeyPrefix = "", + Hosts = new RedisHost[] + { + new RedisHost(){ Host = address, Port = port }, + }, + AllowAdmin = true, + ConnectTimeout = options.ConnectTimeout, + Database = 0, + Ssl = false, + Password = options.Password, + ServerEnumerationStrategy = new ServerEnumerationStrategy() + { + Mode = ServerEnumerationStrategy.ModeOptions.All, + TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any, + UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw, + }, + MaxValueLength = 1024, + PoolSize = mareConfig.GetValue(nameof(ServerConfiguration.RedisPool), 50), + SyncTimeout = options.SyncTimeout, + }; + + services.AddStackExchangeRedisExtensions(redisConfiguration); + // Also expose raw multiplexer for custom Redis usage (discovery presence) + services.AddSingleton(_ => ConnectionMultiplexer.Connect(options)); + } + private void ConfigureConfigServices(IServiceCollection services) + { + services.AddSingleton, MareConfigurationServiceServer>(); + services.AddSingleton, MareConfigurationServiceServer>(); + } + + private void ConfigureDatabase(IServiceCollection services, IConfigurationSection mareConfig) + { + services.AddDbContextPool(options => + { + options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }, mareConfig.GetValue(nameof(MareConfigurationBase.DbContextPoolSize), 1024)); + services.AddDbContextFactory(options => + { + options.UseNpgsql(_configuration.GetConnectionString("DefaultConnection"), builder => + { + builder.MigrationsHistoryTable("_efmigrationshistory", "public"); + builder.MigrationsAssembly("MareSynchronosShared"); + }).UseSnakeCaseNamingConvention(); + options.EnableThreadSafetyChecks(false); + }); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs new file mode 100644 index 0000000..cdb302b --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.ClientStubs.cs @@ -0,0 +1,65 @@ +using MareSynchronos.API.Data; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; +using MareSynchronos.API.Dto.Chat; +using MareSynchronos.API.Dto.Group; +using MareSynchronos.API.Dto.User; + +namespace MareSynchronosServer.Hubs +{ + public partial class MareHub + { + public Task Client_DownloadReady(Guid requestId) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupChatMsg(GroupChatMsgDto groupChatMsgDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupDelete(GroupDto groupDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairChangePermissions(GroupPairUserPermissionDto permissionDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairChangeUserInfo(GroupPairUserInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupPairLeft(GroupPairDto groupPairDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GroupSendInfo(GroupInfoDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_ReceiveServerMessage(MessageSeverity messageSeverity, string message) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserAddClientPair(UserPairDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserChatMsg(UserChatMsgDto userChatMsgDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserReceiveUploadStatus(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserRemoveClientPair(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserSendOffline(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserSendOnline(OnlineUserIdentDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserUpdateProfile(UserDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_UserTypingState(TypingStateDto dto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + + public Task Client_GposeLobbyJoin(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyLeave(UserData userData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyPushCharacterData(CharaDataDownloadDto charaDownloadDto) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyPushPoseData(UserData userData, PoseData poseData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + public Task Client_GposeLobbyPushWorldData(UserData userData, WorldData worldData) => throw new PlatformNotSupportedException("Calling clientside method on server not supported"); + } +} diff --git a/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Typing.cs b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Typing.cs new file mode 100644 index 0000000..f16e296 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.Typing.cs @@ -0,0 +1,36 @@ +using MareSynchronos.API.Dto.User; +using MareSynchronosServer.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.EntityFrameworkCore; + +namespace MareSynchronosServer.Hubs; + +public partial class MareHub +{ + [Authorize(Policy = "Identified")] + public async Task UserSetTypingState(bool isTyping) + { + _logger.LogCallInfo(MareHubLogger.Args(isTyping)); + + var pairedEntries = await GetAllPairedClientsWithPauseState().ConfigureAwait(false); + if (pairedEntries.Count == 0) + return; + + var recipients = pairedEntries + .Where(p => !p.IsPaused) + .Select(p => p.UID) + .Distinct(StringComparer.Ordinal) + .ToList(); + + if (recipients.Count == 0) + return; + + var sender = await DbContext.Users.AsNoTracking() + .SingleAsync(u => u.UID == UserUID) + .ConfigureAwait(false); + + var typingDto = new TypingStateDto(sender.ToUserData(), isTyping); + + await Clients.Users(recipients).Client_UserTypingState(typingDto).ConfigureAwait(false); + } +}