diff --git a/DCGEngine.Database/DCGEDbContext.cs b/DCGEngine.Database/DCGEDbContext.cs index 1f9dc89..3c52370 100644 --- a/DCGEngine.Database/DCGEDbContext.cs +++ b/DCGEngine.Database/DCGEDbContext.cs @@ -58,6 +58,7 @@ public class DCGEDbContext : DbContext IEnumerable pendingMigrations = Database.GetPendingMigrations(); if (!pendingMigrations.Any()) { + _logger.LogDebug("No pending migrations found, continuing."); return; } diff --git a/SVSim.Database/Enums/SocialAccountType.cs b/SVSim.Database/Enums/SocialAccountType.cs new file mode 100644 index 0000000..39f8fcd --- /dev/null +++ b/SVSim.Database/Enums/SocialAccountType.cs @@ -0,0 +1,12 @@ +namespace SVSim.Database.Enums; + +public enum SocialAccountType +{ + None, + GooglePlay, + GameCenter, + Facebook, + Dmm, + Steam, + AppleId +} \ No newline at end of file diff --git a/SVSim.Database/Migrations/20240907191709_Initial.Designer.cs b/SVSim.Database/Migrations/20240907191709_Initial.Designer.cs new file mode 100644 index 0000000..eb37d74 --- /dev/null +++ b/SVSim.Database/Migrations/20240907191709_Initial.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SVSim.Database; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + [DbContext(typeof(SVSimDbContext))] + [Migration("20240907191709_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("DCGEngine.Database.Models.CardEntry", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Attack") + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Defense") + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("TEXT"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryResourceCost") + .HasColumnType("INTEGER"); + + b.Property("ShadowverseDeckEntryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShadowverseDeckEntryId"); + + b.ToTable("CardEntry"); + + b.HasDiscriminator().HasValue("CardEntry"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ShadowverseDeckEntry"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SocialAccountConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AccountType") + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("ViewerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId"); + + b.ToTable("SocialAccountConnection"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShortUdid") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Viewer"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.HasBaseType("DCGEngine.Database.Models.CardEntry"); + + b.HasDiscriminator().HasValue("ShadowverseCardEntry"); + }); + + modelBuilder.Entity("DCGEngine.Database.Models.CardEntry", b => + { + b.HasOne("SVSim.Database.Models.ShadowverseDeckEntry", null) + .WithMany("Cards") + .HasForeignKey("ShadowverseDeckEntryId"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SocialAccountConnection", b => + { + b.HasOne("SVSim.Database.Models.Viewer", "Viewer") + .WithMany() + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Viewer"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.Navigation("Cards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SVSim.Database/Migrations/20240907191709_Initial.cs b/SVSim.Database/Migrations/20240907191709_Initial.cs new file mode 100644 index 0000000..82b9e78 --- /dev/null +++ b/SVSim.Database/Migrations/20240907191709_Initial.cs @@ -0,0 +1,118 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ShadowverseDeckEntry", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DateCreated = table.Column(type: "TEXT", nullable: true), + DateUpdated = table.Column(type: "TEXT", nullable: true), + InternalName = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShadowverseDeckEntry", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Viewer", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DisplayName = table.Column(type: "TEXT", nullable: false), + ShortUdid = table.Column(type: "INTEGER", nullable: false), + DateCreated = table.Column(type: "TEXT", nullable: true), + DateUpdated = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Viewer", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CardEntry", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false), + InternalName = table.Column(type: "TEXT", nullable: false), + Attack = table.Column(type: "INTEGER", nullable: true), + Defense = table.Column(type: "INTEGER", nullable: true), + PrimaryResourceCost = table.Column(type: "INTEGER", nullable: true), + Discriminator = table.Column(type: "TEXT", maxLength: 21, nullable: false), + ShadowverseDeckEntryId = table.Column(type: "INTEGER", nullable: true), + DateCreated = table.Column(type: "TEXT", nullable: true), + DateUpdated = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CardEntry", x => x.Id); + table.ForeignKey( + name: "FK_CardEntry_ShadowverseDeckEntry_ShadowverseDeckEntryId", + column: x => x.ShadowverseDeckEntryId, + principalTable: "ShadowverseDeckEntry", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "SocialAccountConnection", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + AccountType = table.Column(type: "INTEGER", nullable: false), + AccountId = table.Column(type: "INTEGER", nullable: false), + ViewerId = table.Column(type: "INTEGER", nullable: false), + DateCreated = table.Column(type: "TEXT", nullable: true), + DateUpdated = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SocialAccountConnection", x => x.Id); + table.ForeignKey( + name: "FK_SocialAccountConnection_Viewer_ViewerId", + column: x => x.ViewerId, + principalTable: "Viewer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CardEntry_ShadowverseDeckEntryId", + table: "CardEntry", + column: "ShadowverseDeckEntryId"); + + migrationBuilder.CreateIndex( + name: "IX_SocialAccountConnection_ViewerId", + table: "SocialAccountConnection", + column: "ViewerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CardEntry"); + + migrationBuilder.DropTable( + name: "SocialAccountConnection"); + + migrationBuilder.DropTable( + name: "ShadowverseDeckEntry"); + + migrationBuilder.DropTable( + name: "Viewer"); + } + } +} diff --git a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs new file mode 100644 index 0000000..60bb13b --- /dev/null +++ b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs @@ -0,0 +1,168 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SVSim.Database; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + [DbContext(typeof(SVSimDbContext))] + partial class SVSimDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("DCGEngine.Database.Models.CardEntry", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Attack") + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("Defense") + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("TEXT"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PrimaryResourceCost") + .HasColumnType("INTEGER"); + + b.Property("ShadowverseDeckEntryId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ShadowverseDeckEntryId"); + + b.ToTable("CardEntry"); + + b.HasDiscriminator().HasValue("CardEntry"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("InternalName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ShadowverseDeckEntry"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SocialAccountConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AccountId") + .HasColumnType("INTEGER"); + + b.Property("AccountType") + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("ViewerId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId"); + + b.ToTable("SocialAccountConnection"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ShortUdid") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Viewer"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.HasBaseType("DCGEngine.Database.Models.CardEntry"); + + b.HasDiscriminator().HasValue("ShadowverseCardEntry"); + }); + + modelBuilder.Entity("DCGEngine.Database.Models.CardEntry", b => + { + b.HasOne("SVSim.Database.Models.ShadowverseDeckEntry", null) + .WithMany("Cards") + .HasForeignKey("ShadowverseDeckEntryId"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SocialAccountConnection", b => + { + b.HasOne("SVSim.Database.Models.Viewer", "Viewer") + .WithMany() + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Viewer"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.Navigation("Cards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SVSim.Database/Models/SocialAccountConnection.cs b/SVSim.Database/Models/SocialAccountConnection.cs new file mode 100644 index 0000000..fcd295d --- /dev/null +++ b/SVSim.Database/Models/SocialAccountConnection.cs @@ -0,0 +1,25 @@ +using DCGEngine.Database.Models; +using SVSim.Database.Enums; + +namespace SVSim.Database.Models; + +/// +/// A connection between a social account (ie facebook) and a viewer. +/// +public class SocialAccountConnection : BaseEntity +{ + /// + /// The type of the social account. + /// + public SocialAccountType AccountType { get; set; } + + /// + /// The identifier of the social account. + /// + public ulong AccountId { get; set; } + + /// + /// The viewer connected. + /// + public Viewer Viewer { get; set; } +} \ No newline at end of file diff --git a/SVSim.Database/Models/User.cs b/SVSim.Database/Models/User.cs deleted file mode 100644 index 2cf91ff..0000000 --- a/SVSim.Database/Models/User.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DCGEngine.Database.Models; - -namespace SVSim.Database.Models; - -/// -/// A user within the game system. -/// -public class User : BaseEntity -{ - public string ViewerId { get; set; } - public ulong SteamId { get; set; } - public string DisplayName { get; set; } - - -} \ No newline at end of file diff --git a/SVSim.Database/Models/Viewer.cs b/SVSim.Database/Models/Viewer.cs new file mode 100644 index 0000000..555ac5d --- /dev/null +++ b/SVSim.Database/Models/Viewer.cs @@ -0,0 +1,19 @@ +using DCGEngine.Database.Models; + +namespace SVSim.Database.Models; + +/// +/// A user within the game system. +/// +public class Viewer : BaseEntity +{ + /// + /// This user's name displayed in game. + /// + public string DisplayName { get; set; } + + /// + /// This user's short identifier. + /// + public ulong ShortUdid { get; set; } +} \ No newline at end of file diff --git a/SVSim.Database/Repositories/Viewer/IViewerRepository.cs b/SVSim.Database/Repositories/Viewer/IViewerRepository.cs new file mode 100644 index 0000000..4acf5e5 --- /dev/null +++ b/SVSim.Database/Repositories/Viewer/IViewerRepository.cs @@ -0,0 +1,8 @@ +using SVSim.Database.Enums; + +namespace SVSim.Database.Repositories.Viewer; + +public interface IViewerRepository +{ + Task GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId); +} \ No newline at end of file diff --git a/SVSim.Database/Repositories/Viewer/ViewerRepository.cs b/SVSim.Database/Repositories/Viewer/ViewerRepository.cs new file mode 100644 index 0000000..a404dcf --- /dev/null +++ b/SVSim.Database/Repositories/Viewer/ViewerRepository.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using SVSim.Database.Enums; +using SVSim.Database.Models; + +namespace SVSim.Database.Repositories.Viewer; + +public class ViewerRepository : IViewerRepository +{ + protected readonly SVSimDbContext _dbContext; + + public ViewerRepository(SVSimDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId) + { + return _dbContext.Set() + .AsNoTracking() + .Include(sac => sac.Viewer) + .FirstOrDefault(sac => sac.AccountType == accountType && sac.AccountId == socialId) + ?.Viewer; + } +} \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Constants/NetworkConstants.cs b/SVSim.EmulatedEntrypoint/Constants/NetworkConstants.cs new file mode 100644 index 0000000..3f0243c --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Constants/NetworkConstants.cs @@ -0,0 +1,8 @@ +namespace SVSim.EmulatedEntrypoint.Constants; + +public static class NetworkConstants +{ + public const string UdidHeaderName = "UDID"; + public const string SessionIdHeaderName = "SID"; + public const string ShortUdidHeaderName = "SHORT_UDID"; +} \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Constants/ShadowverseClaimTypes.cs b/SVSim.EmulatedEntrypoint/Constants/ShadowverseClaimTypes.cs new file mode 100644 index 0000000..ad4778c --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Constants/ShadowverseClaimTypes.cs @@ -0,0 +1,7 @@ +namespace SVSim.EmulatedEntrypoint.Constants; + +public static class ShadowverseClaimTypes +{ + public const string ShortUdidClaim = "ShortUdid"; + public const string ViewerIdClaim = "ViewerId"; +} \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Controllers/CheckController.cs b/SVSim.EmulatedEntrypoint/Controllers/CheckController.cs index e11a1c1..bf55905 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/CheckController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/CheckController.cs @@ -1,6 +1,7 @@ using System.Buffers.Text; using System.Text; using MessagePack; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; @@ -22,6 +23,7 @@ namespace SVSim.EmulatedEntrypoint.Controllers _logger = logger; } + [AllowAnonymous] [HttpPost("special_title")] public async Task> SpecialTitleCheck(SpecialTitleCheckRequest request) { diff --git a/SVSim.EmulatedEntrypoint/Controllers/SVSimController.cs b/SVSim.EmulatedEntrypoint/Controllers/SVSimController.cs index 4a00e48..ace54fc 100644 --- a/SVSim.EmulatedEntrypoint/Controllers/SVSimController.cs +++ b/SVSim.EmulatedEntrypoint/Controllers/SVSimController.cs @@ -1,6 +1,8 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using SVSim.EmulatedEntrypoint.Security; +using SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication; namespace SVSim.EmulatedEntrypoint.Controllers { @@ -9,11 +11,8 @@ namespace SVSim.EmulatedEntrypoint.Controllers /// [Route("api/[controller]")] [ApiController] + [Authorize(AuthenticationSchemes = SteamAuthenticationConstants.SchemeName)] public abstract class SVSimController : ControllerBase { - /// - /// Returns the UdId of the user making the request. Can be null or empty, as only certain requests will send it. Known requests to send this value are: SignUp, CheckSpecialTitle, CheckiCloudUser, MigrateiCloudUser - /// - public string? UdId => Encryption.Decode(Request.Headers["UDID"]); } } diff --git a/SVSim.EmulatedEntrypoint/Middlewares/SessionidMappingMiddleware.cs b/SVSim.EmulatedEntrypoint/Middlewares/SessionidMappingMiddleware.cs new file mode 100644 index 0000000..86c280e --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Middlewares/SessionidMappingMiddleware.cs @@ -0,0 +1,30 @@ +using SVSim.EmulatedEntrypoint.Constants; +using SVSim.EmulatedEntrypoint.Security; +using SVSim.EmulatedEntrypoint.Services; + +namespace SVSim.EmulatedEntrypoint.Middlewares; + +/// +/// Maps an incoming request's session id to a udid if both are present. +/// +public class SessionidMappingMiddleware : IMiddleware +{ + private readonly ShadowverseSessionService _shadowverseSessionService; + + public SessionidMappingMiddleware(ShadowverseSessionService shadowverseSessionService) + { + _shadowverseSessionService = shadowverseSessionService; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + bool hasSessionId = context.Request.Headers.TryGetValue(NetworkConstants.UdidHeaderName, out var udid); + bool hasUdid = context.Request.Headers.TryGetValue(NetworkConstants.SessionIdHeaderName, out var sid); + if (hasSessionId && hasUdid) + { + _shadowverseSessionService.StoreUdidForSessionId(sid.FirstOrDefault(), Guid.Parse(Encryption.Decode(udid.FirstOrDefault()))); + } + + await next.Invoke(context); + } +} \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs b/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs index c2e1b2d..cd15527 100644 --- a/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs +++ b/SVSim.EmulatedEntrypoint/Middlewares/ShadowverseTranslationMiddleware.cs @@ -4,7 +4,9 @@ using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; +using SVSim.EmulatedEntrypoint.Constants; using SVSim.EmulatedEntrypoint.Security; +using SVSim.EmulatedEntrypoint.Services; namespace SVSim.EmulatedEntrypoint.Middlewares; @@ -14,10 +16,12 @@ namespace SVSim.EmulatedEntrypoint.Middlewares; public class ShadowverseTranslationMiddleware : IMiddleware { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + private readonly ShadowverseSessionService _sessionService; - public ShadowverseTranslationMiddleware(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) + public ShadowverseTranslationMiddleware(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, ShadowverseSessionService sessionService) { _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; + _sessionService = sessionService; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -32,21 +36,33 @@ public class ShadowverseTranslationMiddleware : IMiddleware await next.Invoke(context); return; } - using var requestBytesStream = new MemoryStream(); + + // Replace response body stream to re-access it. using var tempResponseBody = new MemoryStream(); var originalResponsebody = context.Response.Body; context.Response.Body = tempResponseBody; + + // Pull out the request bytes into a stream + using var requestBytesStream = new MemoryStream(); await context.Request.Body.CopyToAsync(requestBytesStream); byte[] requestBytes = requestBytesStream.ToArray(); - // Decrypt incoming data. Placeholder. - requestBytes = Encryption.Decrypt(requestBytes, Encryption.Decode(context.Request.Headers["UDID"])); + + // Get encryption values for this request + string sid = context.Request.Headers[NetworkConstants.SessionIdHeaderName]; + string udid = _sessionService.GetUdidFromSessionId(sid).GetValueOrDefault().ToString(); + + // Decrypt incoming data. + requestBytes = Encryption.Decrypt(requestBytes, udid); object? data = MessagePackSerializer.Deserialize(endpointDescriptor.Parameters.FirstOrDefault().ParameterType, requestBytes); var json = JsonConvert.SerializeObject(data); var newStream = new StringContent(json, Encoding.UTF8, "application/json"); context.Request.Body = newStream.ReadAsStream(); context.Request.Headers.ContentType = new StringValues("application/json"); + await next.Invoke(context); + + // Convert the response into a messagepack, encrypt it var responseType = ((ControllerActionDescriptor)endpointDescriptor).MethodInfo.ReturnType; if (responseType.IsGenericType && responseType.GetGenericTypeDefinition() == typeof(Task<>)) { @@ -58,7 +74,7 @@ public class ShadowverseTranslationMiddleware : IMiddleware var responseBytes = responseBytesStream.ToArray(); var responseData = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(responseBytes), responseType); var packedData = MessagePackSerializer.Serialize(responseType, responseData); - packedData = Encryption.Encrypt(packedData, Encryption.Decode(context.Request.Headers["UDID"])); + packedData = Encryption.Encrypt(packedData, udid); await originalResponsebody.WriteAsync(Encoding.UTF8.GetBytes(Convert.ToBase64String(packedData))); context.Response.Body = originalResponsebody; } diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/BaseRequest.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/BaseRequest.cs index c7a87bb..e63c488 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/BaseRequest.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Requests/BaseRequest.cs @@ -3,12 +3,12 @@ using MessagePack; namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests; [MessagePackObject] -public abstract class BaseRequest +public class BaseRequest { [Key("viewer_id")] public string ViewerId { get; set; } [Key("steam_id")] - public long SteamId { get; set; } + public ulong SteamId { get; set; } [Key("steam_session_ticket")] public string SteamSessionTicket { get; set; } } \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/TransitionAccountData.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/TransitionAccountData.cs index 5b5d0f2..4998d92 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/TransitionAccountData.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/TransitionAccountData.cs @@ -7,8 +7,10 @@ public class TransitionAccountData { [Key("social_account_id")] public string SocialAccountId { get; set; } + [Key("social_account_type")] public string SocialAccountType { get; set; } + [Key("connected_viewer_id")] public string ConnectedViewerId { get; set; } } \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Program.cs b/SVSim.EmulatedEntrypoint/Program.cs index ed9472c..e482595 100644 --- a/SVSim.EmulatedEntrypoint/Program.cs +++ b/SVSim.EmulatedEntrypoint/Program.cs @@ -4,7 +4,10 @@ using DCGEngine.Database.Configuration; using Microsoft.EntityFrameworkCore; using SVSim.Database; using SVSim.Database.Models; +using SVSim.Database.Repositories.Viewer; using SVSim.EmulatedEntrypoint.Middlewares; +using SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication; +using SVSim.EmulatedEntrypoint.Services; namespace SVSim.EmulatedEntrypoint; @@ -24,17 +27,41 @@ public class Program { }); + + #region Database Services + builder.Services.AddDbContext(opt => { - opt.UseSqlite(); + opt.UseSqlite(builder.Configuration.GetConnectionString("Sqlite")); }); + builder.Services.AddTransient(); + + #endregion + builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.Configure(opt => { opt.DbSetSearchAssemblies = new List { Assembly.GetAssembly(typeof(SVSimDbContext)) }; }); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddAuthentication() + .AddScheme( + SteamAuthenticationConstants.SchemeName, + opt => + { + + }); var app = builder.Build(); + + // Update database + using (var scope = app.Services.CreateScope()) + { + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.UpdateDatabase(); + } app.UseHttpLogging(); @@ -47,8 +74,12 @@ public class Program //app.UseHttpsRedirection(); + app.UseMiddleware(); + app.UseMiddleware(); + app.UseAuthentication(); + app.UseAuthorization(); diff --git a/SVSim.EmulatedEntrypoint/SVSim.EmulatedEntrypoint.csproj b/SVSim.EmulatedEntrypoint/SVSim.EmulatedEntrypoint.csproj index 54161cb..04c951a 100644 --- a/SVSim.EmulatedEntrypoint/SVSim.EmulatedEntrypoint.csproj +++ b/SVSim.EmulatedEntrypoint/SVSim.EmulatedEntrypoint.csproj @@ -32,4 +32,29 @@ + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + + + + + diff --git a/SVSim.EmulatedEntrypoint/Security/Encryption.cs b/SVSim.EmulatedEntrypoint/Security/Encryption.cs index 18e27d8..de849d3 100644 --- a/SVSim.EmulatedEntrypoint/Security/Encryption.cs +++ b/SVSim.EmulatedEntrypoint/Security/Encryption.cs @@ -58,27 +58,29 @@ public static class Encryption /// the decrypted bytes public static byte[] Decrypt(byte[] encryptedData, string udId) { - using (var rj = Aes.Create()) + using (var rj = new RijndaelManaged()) { rj.KeySize = EncryptionKeySize; rj.Mode = EncryptionMode; rj.BlockSize = EncryptionBlockSize; + //rj.Padding = PaddingMode.None; byte[] rgbIv = Encoding.UTF8.GetBytes(udId.Replace("-", string.Empty).Substring(0, UdIdKeySize)); byte[] keyBytes = new byte[KeyStringSize]; byte[] encryptedValueBytes = new byte[encryptedData.Length - KeyStringSize]; Array.Copy(encryptedData, encryptedData.Length - keyBytes.Length, keyBytes, 0, keyBytes.Length); Array.Copy(encryptedData, 0, encryptedValueBytes, 0, encryptedValueBytes.Length); ICryptoTransform transform = rj.CreateDecryptor(keyBytes, rgbIv); + byte[] decryptedValueBytes = new byte[encryptedValueBytes.Length]; using (MemoryStream ms = new MemoryStream(encryptedValueBytes)) { using (CryptoStream cs = new CryptoStream(ms, transform, CryptoStreamMode.Read)) { - byte[] decryptedValueBytes = new byte[encryptedValueBytes.Length]; - cs.Read(decryptedValueBytes, 0, encryptedValueBytes.Length); - cs.FlushFinalBlock(); - return decryptedValueBytes; + cs.CopyTo(decryptedValueBytes); + cs.Flush(); + ms.Flush(); } } + return decryptedValueBytes; } } diff --git a/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamAuthenticationConstants.cs b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamAuthenticationConstants.cs new file mode 100644 index 0000000..62a033e --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamAuthenticationConstants.cs @@ -0,0 +1,7 @@ +namespace SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication; + +public static class SteamAuthenticationConstants +{ + public const string SchemeName = "SteamAuthentication"; + public const string SteamIdClaim = "SteamId"; +} \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs index 545fcf7..532a893 100644 --- a/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs +++ b/SVSim.EmulatedEntrypoint/Security/SteamSessionAuthentication/SteamSessionAuthenticationHandler.cs @@ -1,21 +1,74 @@ +using System.Security.Claims; +using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.Database.Repositories.Viewer; +using SVSim.EmulatedEntrypoint.Constants; +using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; +using SVSim.EmulatedEntrypoint.Services; namespace SVSim.EmulatedEntrypoint.Security.SteamSessionAuthentication; public class SteamSessionAuthenticationHandler : AuthenticationHandler { - public SteamSessionAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) - { - } - - public SteamSessionAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) + private readonly SteamSessionService _sessionService; + private readonly IViewerRepository _viewerRepository; + public SteamSessionAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, SteamSessionService sessionService, IViewerRepository viewerRepository) : base(options, logger, encoder) { + _sessionService = sessionService; + _viewerRepository = viewerRepository; } protected async override Task HandleAuthenticateAsync() { - return AuthenticateResult.Fail("Not implemented"); + byte[] requestBytes; + using (var requestBytesStream = new MemoryStream()) + { + await Request.Body.CopyToAsync(requestBytesStream); + requestBytes = requestBytesStream.ToArray(); + } + + // Convert bytes to json + string requestString = Encoding.UTF8.GetString(requestBytes); + BaseRequest? requestJson = JsonConvert.DeserializeObject(requestString); + + // Reset request stream + Request.Body.Seek(0, SeekOrigin.Begin); + + if (requestJson is null) + { + return AuthenticateResult.Fail("Invalid request body."); + } + + // Check steam session validity + bool sessionIsValid = _sessionService.IsTicketValidForUser(requestJson.SteamSessionTicket, requestJson.SteamId); + if (!sessionIsValid) + { + return AuthenticateResult.Fail("Invalid ticket."); + } + + Viewer? viewer = + await _viewerRepository.GetViewerBySocialConnection(SocialAccountType.Steam, requestJson.SteamId); + + if (viewer is null) + { + return AuthenticateResult.Fail("User not found."); + } + + // Build identity + ClaimsIdentity identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(ClaimTypes.Name, viewer.DisplayName)); + identity.AddClaim(new Claim(ShadowverseClaimTypes.ShortUdidClaim, viewer.ShortUdid.ToString())); + identity.AddClaim(new Claim(ShadowverseClaimTypes.ViewerIdClaim, viewer.Id.ToString())); + identity.AddClaim(new Claim(SteamAuthenticationConstants.SteamIdClaim, requestJson.SteamId.ToString())); + + // Build and return final ticket + AuthenticationTicket ticket = + new AuthenticationTicket(new ClaimsPrincipal(), SteamAuthenticationConstants.SchemeName); + return AuthenticateResult.Success(ticket); } } \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/Services/ShadowverseSessionService.cs b/SVSim.EmulatedEntrypoint/Services/ShadowverseSessionService.cs new file mode 100644 index 0000000..54a883b --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Services/ShadowverseSessionService.cs @@ -0,0 +1,28 @@ +using System.Collections.Concurrent; + +namespace SVSim.EmulatedEntrypoint.Services; + +public class ShadowverseSessionService +{ + private readonly ConcurrentDictionary _sessionIdToUdid; + + public ShadowverseSessionService() + { + _sessionIdToUdid = new(); + } + + public Guid? GetUdidFromSessionId(string sid) + { + if (_sessionIdToUdid.TryGetValue(sid, out var udid)) + { + return udid; + } + + return null; + } + + public void StoreUdidForSessionId(string sid, Guid udid) + { + _sessionIdToUdid.AddOrUpdate(sid, _ => udid, (_, _) => udid); + } +} \ No newline at end of file diff --git a/SVSim.EmulatedEntrypoint/WeatherForecast.cs b/SVSim.EmulatedEntrypoint/WeatherForecast.cs deleted file mode 100644 index e9ba8c1..0000000 --- a/SVSim.EmulatedEntrypoint/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SVSim.EmulatedEntrypoint; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} diff --git a/SVSim.EmulatedEntrypoint/appsettings.json b/SVSim.EmulatedEntrypoint/appsettings.json index bb20fb6..f7d70f3 100644 --- a/SVSim.EmulatedEntrypoint/appsettings.json +++ b/SVSim.EmulatedEntrypoint/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information" } }, + "ConnectionStrings": { + "Sqlite": "Data Source=test_db" + }, "AllowedHosts": "*" } diff --git a/SVSim.EmulatedEntrypoint/lib/libsteam_api.so b/SVSim.EmulatedEntrypoint/lib/libsteam_api.so new file mode 100644 index 0000000..33762a7 Binary files /dev/null and b/SVSim.EmulatedEntrypoint/lib/libsteam_api.so differ diff --git a/SVSim.EmulatedEntrypoint/lib/steam_api.dll b/SVSim.EmulatedEntrypoint/lib/steam_api.dll index 7fde3a3..6e0f440 100644 Binary files a/SVSim.EmulatedEntrypoint/lib/steam_api.dll and b/SVSim.EmulatedEntrypoint/lib/steam_api.dll differ diff --git a/SVSim.EmulatedEntrypoint/lib/steam_api.lib b/SVSim.EmulatedEntrypoint/lib/steam_api.lib new file mode 100644 index 0000000..66182e3 Binary files /dev/null and b/SVSim.EmulatedEntrypoint/lib/steam_api.lib differ diff --git a/SVSim.EmulatedEntrypoint/lib/steam_api64.dll b/SVSim.EmulatedEntrypoint/lib/steam_api64.dll index 9ad453c..ad13f2b 100644 Binary files a/SVSim.EmulatedEntrypoint/lib/steam_api64.dll and b/SVSim.EmulatedEntrypoint/lib/steam_api64.dll differ diff --git a/SVSim.EmulatedEntrypoint/lib/steam_api64.lib b/SVSim.EmulatedEntrypoint/lib/steam_api64.lib new file mode 100644 index 0000000..9be697b Binary files /dev/null and b/SVSim.EmulatedEntrypoint/lib/steam_api64.lib differ diff --git a/SVSim.UnitTests/UnitTest1.cs b/SVSim.UnitTests/UnitTest1.cs index 33e6998..8060ec4 100644 --- a/SVSim.UnitTests/UnitTest1.cs +++ b/SVSim.UnitTests/UnitTest1.cs @@ -16,6 +16,6 @@ public class Tests "140000005ee7d30c1263e214e133a10001001001e07cd866180000000100000002000000b8526bb7b8946cd27c214574f1000000b20000003200000004000000e133a1000100100168eb0600488cc2443101a8c0000000008165d4660115f06601005c7e010000000000cad61456a2b83d39595c3e3749b96b4537ebde88d048103a6f6c7b2b81ee68711378836872a11422f5bd16fad803f81122c5ae98d986b693bbbc00ac7d30a8f85af2c1a7dce57751eb2c7f21130284aa8d9ee787246c8ccc138f05936bacb1ba4baba5fa5fbf6158002cf7207ae25a6f6ee8e3fc8edbb84903d346a249179637"; using var steamService = new SteamSessionService(); bool validTicket = steamService.IsTicketValidForUser(ticket, 76561197970830305); - Assert.AreEqual(true, validTicket); + Assert.That(validTicket, Is.EqualTo(true)); } } \ No newline at end of file