Moved database-related logic to own project

Moved tests into their own respective project-folder
Moved most composition logic to their own respective project
Moved Cobra.Hook to its own solution
Added almost all database models based on existing ERD
Added tests for SteamAuthMiddleware
Added UserService to retrieve the user associated with the current request
Added HitmanUserService to perform user-specific actions on the database
Added start of actual multi-user HitmanServer implemention
Cleaned up SteamAuthMiddleware based on tests
This commit is contained in:
Lennard Fonteijn 2023-10-27 01:23:42 +02:00
parent dedb008c81
commit 107edbea8a
54 changed files with 2205 additions and 295 deletions

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="LocalDatabase.db" uuid="e5c25f83-a441-487d-9e27-a6bb95bfddcc">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/Src/Cobra.Server/Data/LocalDatabase.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

43
Cobra.Hook.sln Normal file
View File

@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33122.133
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{43E1351B-28D0-45FE-A10A-545CFE650039}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
README.md = README.md
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Cobra.Hook", "Src\Cobra.Hook\Cobra.Hook.vcxproj", "{57ABB826-E2F3-4908-BBBB-634C1A747CAA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|Any CPU.ActiveCfg = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|Any CPU.Build.0 = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x64.ActiveCfg = Debug|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x64.Build.0 = Debug|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x86.ActiveCfg = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x86.Build.0 = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|Any CPU.ActiveCfg = Release|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|Any CPU.Build.0 = Release|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x64.ActiveCfg = Release|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x64.Build.0 = Release|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x86.ActiveCfg = Release|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {742A6A34-A300-4A5A-BD85-0CBCC7421E7A}
EndGlobalSection
EndGlobal

View File

@ -12,8 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentat
README.md = README.md
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Cobra.Hook", "Src\Cobra.Hook\Cobra.Hook.vcxproj", "{57ABB826-E2F3-4908-BBBB-634C1A747CAA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cobra.Test", "Src\Cobra.Test\Cobra.Test.csproj", "{815C296D-7137-4DC6-96FF-4EC7432CEA1F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cobra.Analyzer", "Src\Cobra.Analyzer\Cobra.Analyzer.csproj", "{EC31FDCB-1EF1-4B39-8404-5E3BAF26A3D2}"
@ -28,7 +26,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D1082260
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cobra.Server.Sniper", "Src\Cobra.Server.Sniper\Cobra.Server.Sniper.csproj", "{9D3CA9E4-4C90-46F6-9638-1A51380ACEAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cobra.Server.Hitman", "Src\Cobra.Server.Hitman\Cobra.Server.Hitman.csproj", "{32FEBCBD-2000-4097-A01D-A9423C920354}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cobra.Server.Hitman", "Src\Cobra.Server.Hitman\Cobra.Server.Hitman.csproj", "{32FEBCBD-2000-4097-A01D-A9423C920354}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cobra.Server.Database", "Src\Cobra.Server.Database\Cobra.Server.Database.csproj", "{0AC11ABA-A1C0-4027-A19F-485F17DE6580}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -52,18 +52,6 @@ Global
{6FA95780-4890-46C7-9342-C5CC7E465EEF}.Release|x64.Build.0 = Release|Any CPU
{6FA95780-4890-46C7-9342-C5CC7E465EEF}.Release|x86.ActiveCfg = Release|Any CPU
{6FA95780-4890-46C7-9342-C5CC7E465EEF}.Release|x86.Build.0 = Release|Any CPU
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|Any CPU.ActiveCfg = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|Any CPU.Build.0 = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x64.ActiveCfg = Debug|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x64.Build.0 = Debug|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x86.ActiveCfg = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Debug|x86.Build.0 = Debug|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|Any CPU.ActiveCfg = Release|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|Any CPU.Build.0 = Release|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x64.ActiveCfg = Release|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x64.Build.0 = Release|x64
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x86.ActiveCfg = Release|Win32
{57ABB826-E2F3-4908-BBBB-634C1A747CAA}.Release|x86.Build.0 = Release|Win32
{815C296D-7137-4DC6-96FF-4EC7432CEA1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{815C296D-7137-4DC6-96FF-4EC7432CEA1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{815C296D-7137-4DC6-96FF-4EC7432CEA1F}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -136,12 +124,23 @@ Global
{32FEBCBD-2000-4097-A01D-A9423C920354}.Release|x64.Build.0 = Release|Any CPU
{32FEBCBD-2000-4097-A01D-A9423C920354}.Release|x86.ActiveCfg = Release|Any CPU
{32FEBCBD-2000-4097-A01D-A9423C920354}.Release|x86.Build.0 = Release|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Debug|x64.ActiveCfg = Debug|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Debug|x64.Build.0 = Debug|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Debug|x86.ActiveCfg = Debug|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Debug|x86.Build.0 = Debug|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Release|Any CPU.Build.0 = Release|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Release|x64.ActiveCfg = Release|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Release|x64.Build.0 = Release|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Release|x86.ActiveCfg = Release|Any CPU
{0AC11ABA-A1C0-4027-A19F-485F17DE6580}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{57ABB826-E2F3-4908-BBBB-634C1A747CAA} = {ADCBE1D4-26D9-48A0-87A1-4BA6D851C9F5}
{815C296D-7137-4DC6-96FF-4EC7432CEA1F} = {D1082260-6FB2-4A86-B8D3-E4B75DC7A3B3}
{EC31FDCB-1EF1-4B39-8404-5E3BAF26A3D2} = {ADCBE1D4-26D9-48A0-87A1-4BA6D851C9F5}
EndGlobalSection

View File

@ -23,6 +23,11 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=leaderboardtype/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=levelindex/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=metacategories/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OSAUTHPROVIDER/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OSAUTHRESPONSE/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OSAUTHTICKETDATA/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OSERROR/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=OSUID/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Silverballer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Splitted/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=startindex/@EntryIndexedValue">True</s:Boolean>

16
Docs/Development.md Normal file
View File

@ -0,0 +1,16 @@
# Development
This document described how to get started with the project.
# Add database migration to project
Run the following command from the `Src\Cobra.Server` folder:
```
dotnet ef migrations add [Name] --project ../Cobra.Server.Database
```
Where `[Name]` is replace with the name of the migration you wish to add.
# Apply database migration to database
Run the following command from the `Src\Cobra.Server` folder:
```
dotnet ef database update --project ../Cobra.Server.Database
```

View File

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Cobra.Analyzer\Cobra.Analyzer.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Analyzer</OutputItemType>
</ProjectReference>
<ProjectReference Include="..\Cobra.Server.Shared\Cobra.Server.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using Cobra.Server.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Cobra.Server.Database
{
public static class Compositions
{
public static void ConfigureServices(IServiceCollection services, IConfiguration configuration, Options options)
{
services.AddDbContextFactory<DatabaseContext>(optionsBuilder =>
{
optionsBuilder
.UseSqlite(
configuration.GetConnectionString("CobraDatabase"),
x => x.MigrationsAssembly("Cobra.Server.Database")
);
});
}
public static void Configure(IServiceProvider services)
{
var databaseContext = services.GetRequiredService<DatabaseContext>();
databaseContext.Database.Migrate();
}
}
}

View File

@ -0,0 +1,23 @@
using Cobra.Server.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database
{
public class DatabaseContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<UserFriend> UserFriends { get; set; }
public DbSet<UserContract> UserContracts { get; set; }
public DbSet<Contract> Contracts { get; set; }
public DbSet<ContractTarget> ContractTargets { get; set; }
public DbSet<ScoreStory> ScoresStory { get; set; }
public DbSet<ScoreSniper> ScoresSniper { get; set; }
public DbSet<ScoreTutorial> ScoresTutorial { get; set; }
public DatabaseContext(DbContextOptions<DatabaseContext> dbContextOptions)
: base(dbContextOptions)
{
//Do nothing
}
}
}

View File

@ -0,0 +1,13 @@
namespace Cobra.Server.Database.Enums
{
[Flags]
public enum EContractRestrictionType : byte
{
None = 0,
TargetOnly = 1,
SuitOnly = 2,
PerfectShooter = 4,
EraseTraces = 8,
NoWitnesses = 16
}
}

View File

@ -0,0 +1,342 @@
// <auto-generated />
using System;
using Cobra.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cobra.Server.Database.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20231026172304_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.12");
modelBuilder.Entity("Cobra.Server.Database.Models.Contract", b =>
{
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<int>("CheckpointIndex")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Difficulty")
.HasColumnType("INTEGER");
b.Property<string>("DisplayId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("ExitId")
.HasColumnType("INTEGER");
b.Property<int>("LevelIndex")
.HasColumnType("INTEGER");
b.Property<int>("OutfitToken")
.HasColumnType("INTEGER");
b.Property<byte>("Restrictions")
.HasColumnType("INTEGER");
b.Property<uint>("Target1Id")
.HasColumnType("INTEGER");
b.Property<uint?>("Target2Id")
.HasColumnType("INTEGER");
b.Property<uint?>("Target3Id")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("WeaponToken")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayId")
.IsUnique();
b.HasIndex("Target1Id");
b.HasIndex("Target2Id");
b.HasIndex("Target3Id");
b.HasIndex("UserId");
b.ToTable("Contracts");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ContractTarget", b =>
{
b.Property<uint>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AmmoType")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("OutfitToken")
.HasColumnType("INTEGER");
b.Property<int>("SpecialSituation")
.HasColumnType("INTEGER");
b.Property<int>("WeaponToken")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Name", "WeaponToken", "OutfitToken", "AmmoType", "SpecialSituation")
.IsUnique();
b.ToTable("ContractTargets");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreSniper", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.HasKey("UserId");
b.ToTable("ScoresSniper");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreStory", b =>
{
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.HasKey("Id", "UserId");
b.HasIndex("UserId");
b.ToTable("ScoresStory");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreTutorial", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.HasKey("UserId");
b.ToTable("ScoresTutorial");
});
modelBuilder.Entity("Cobra.Server.Database.Models.User", b =>
{
b.Property<ulong>("Id")
.HasColumnType("INTEGER");
b.Property<int>("CompetitionPlays")
.HasColumnType("INTEGER");
b.Property<int>("ContractPlays")
.HasColumnType("INTEGER");
b.Property<int>("Country")
.HasColumnType("INTEGER");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Trophies")
.HasColumnType("INTEGER");
b.Property<int>("Wallet")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Country");
b.ToTable("Users");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserContract", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<uint>("ContractId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("TEXT");
b.Property<bool?>("Liked")
.HasColumnType("INTEGER");
b.Property<int?>("Plays")
.HasColumnType("INTEGER");
b.Property<bool>("Queued")
.HasColumnType("INTEGER");
b.Property<int?>("Score")
.HasColumnType("INTEGER");
b.HasKey("UserId", "ContractId");
b.HasIndex("ContractId");
b.HasIndex("UserId");
b.HasIndex("UserId", "Queued");
b.ToTable("UserContracts");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserFriend", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<ulong>("SteamId")
.HasColumnType("INTEGER");
b.HasKey("UserId", "SteamId");
b.ToTable("UserFriends");
});
modelBuilder.Entity("Cobra.Server.Database.Models.Contract", b =>
{
b.HasOne("Cobra.Server.Database.Models.ContractTarget", "Target1")
.WithMany()
.HasForeignKey("Target1Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Cobra.Server.Database.Models.ContractTarget", "Target2")
.WithMany()
.HasForeignKey("Target2Id");
b.HasOne("Cobra.Server.Database.Models.ContractTarget", "Target3")
.WithMany()
.HasForeignKey("Target3Id");
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Target1");
b.Navigation("Target2");
b.Navigation("Target3");
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreSniper", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreStory", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreTutorial", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserContract", b =>
{
b.HasOne("Cobra.Server.Database.Models.Contract", "Contract")
.WithMany()
.HasForeignKey("ContractId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Contract");
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserFriend", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany("Friends")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.User", b =>
{
b.Navigation("Friends");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,282 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cobra.Server.Database.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ContractTargets",
columns: table => new
{
Id = table.Column<uint>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
WeaponToken = table.Column<int>(type: "INTEGER", nullable: false),
OutfitToken = table.Column<int>(type: "INTEGER", nullable: false),
AmmoType = table.Column<int>(type: "INTEGER", nullable: false),
SpecialSituation = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ContractTargets", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<ulong>(type: "INTEGER", nullable: false),
DisplayName = table.Column<string>(type: "TEXT", nullable: false),
Country = table.Column<int>(type: "INTEGER", nullable: false),
Wallet = table.Column<int>(type: "INTEGER", nullable: false),
ContractPlays = table.Column<int>(type: "INTEGER", nullable: false),
CompetitionPlays = table.Column<int>(type: "INTEGER", nullable: false),
Trophies = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Contracts",
columns: table => new
{
Id = table.Column<uint>(type: "INTEGER", nullable: false),
DisplayId = table.Column<string>(type: "TEXT", nullable: false),
Title = table.Column<string>(type: "TEXT", nullable: false),
Description = table.Column<string>(type: "TEXT", nullable: false),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
LevelIndex = table.Column<int>(type: "INTEGER", nullable: false),
CheckpointIndex = table.Column<int>(type: "INTEGER", nullable: false),
Difficulty = table.Column<int>(type: "INTEGER", nullable: false),
ExitId = table.Column<int>(type: "INTEGER", nullable: false),
WeaponToken = table.Column<int>(type: "INTEGER", nullable: false),
OutfitToken = table.Column<int>(type: "INTEGER", nullable: false),
Target1Id = table.Column<uint>(type: "INTEGER", nullable: false),
Target2Id = table.Column<uint>(type: "INTEGER", nullable: true),
Target3Id = table.Column<uint>(type: "INTEGER", nullable: true),
Restrictions = table.Column<byte>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Contracts", x => x.Id);
table.ForeignKey(
name: "FK_Contracts_ContractTargets_Target1Id",
column: x => x.Target1Id,
principalTable: "ContractTargets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Contracts_ContractTargets_Target2Id",
column: x => x.Target2Id,
principalTable: "ContractTargets",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Contracts_ContractTargets_Target3Id",
column: x => x.Target3Id,
principalTable: "ContractTargets",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Contracts_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ScoresSniper",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Score = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ScoresSniper", x => x.UserId);
table.ForeignKey(
name: "FK_ScoresSniper_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ScoresStory",
columns: table => new
{
Id = table.Column<uint>(type: "INTEGER", nullable: false),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Score = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ScoresStory", x => new { x.Id, x.UserId });
table.ForeignKey(
name: "FK_ScoresStory_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ScoresTutorial",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Score = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ScoresTutorial", x => x.UserId);
table.ForeignKey(
name: "FK_ScoresTutorial_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserFriends",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
SteamId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserFriends", x => new { x.UserId, x.SteamId });
table.ForeignKey(
name: "FK_UserFriends_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "UserContracts",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
ContractId = table.Column<uint>(type: "INTEGER", nullable: false),
Queued = table.Column<bool>(type: "INTEGER", nullable: false),
Plays = table.Column<int>(type: "INTEGER", nullable: true),
Score = table.Column<int>(type: "INTEGER", nullable: true),
Liked = table.Column<bool>(type: "INTEGER", nullable: true),
LastPlayedAt = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserContracts", x => new { x.UserId, x.ContractId });
table.ForeignKey(
name: "FK_UserContracts_Contracts_ContractId",
column: x => x.ContractId,
principalTable: "Contracts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_UserContracts_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Contracts_DisplayId",
table: "Contracts",
column: "DisplayId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Contracts_Target1Id",
table: "Contracts",
column: "Target1Id");
migrationBuilder.CreateIndex(
name: "IX_Contracts_Target2Id",
table: "Contracts",
column: "Target2Id");
migrationBuilder.CreateIndex(
name: "IX_Contracts_Target3Id",
table: "Contracts",
column: "Target3Id");
migrationBuilder.CreateIndex(
name: "IX_Contracts_UserId",
table: "Contracts",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_ContractTargets_Name_WeaponToken_OutfitToken_AmmoType_SpecialSituation",
table: "ContractTargets",
columns: new[] { "Name", "WeaponToken", "OutfitToken", "AmmoType", "SpecialSituation" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ScoresStory_UserId",
table: "ScoresStory",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_UserContracts_ContractId",
table: "UserContracts",
column: "ContractId");
migrationBuilder.CreateIndex(
name: "IX_UserContracts_UserId",
table: "UserContracts",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_UserContracts_UserId_Queued",
table: "UserContracts",
columns: new[] { "UserId", "Queued" });
migrationBuilder.CreateIndex(
name: "IX_Users_Country",
table: "Users",
column: "Country");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ScoresSniper");
migrationBuilder.DropTable(
name: "ScoresStory");
migrationBuilder.DropTable(
name: "ScoresTutorial");
migrationBuilder.DropTable(
name: "UserContracts");
migrationBuilder.DropTable(
name: "UserFriends");
migrationBuilder.DropTable(
name: "Contracts");
migrationBuilder.DropTable(
name: "ContractTargets");
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@ -0,0 +1,339 @@
// <auto-generated />
using System;
using Cobra.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cobra.Server.Database.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.12");
modelBuilder.Entity("Cobra.Server.Database.Models.Contract", b =>
{
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<int>("CheckpointIndex")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Difficulty")
.HasColumnType("INTEGER");
b.Property<string>("DisplayId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("ExitId")
.HasColumnType("INTEGER");
b.Property<int>("LevelIndex")
.HasColumnType("INTEGER");
b.Property<int>("OutfitToken")
.HasColumnType("INTEGER");
b.Property<byte>("Restrictions")
.HasColumnType("INTEGER");
b.Property<uint>("Target1Id")
.HasColumnType("INTEGER");
b.Property<uint?>("Target2Id")
.HasColumnType("INTEGER");
b.Property<uint?>("Target3Id")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("WeaponToken")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayId")
.IsUnique();
b.HasIndex("Target1Id");
b.HasIndex("Target2Id");
b.HasIndex("Target3Id");
b.HasIndex("UserId");
b.ToTable("Contracts");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ContractTarget", b =>
{
b.Property<uint>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AmmoType")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("OutfitToken")
.HasColumnType("INTEGER");
b.Property<int>("SpecialSituation")
.HasColumnType("INTEGER");
b.Property<int>("WeaponToken")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Name", "WeaponToken", "OutfitToken", "AmmoType", "SpecialSituation")
.IsUnique();
b.ToTable("ContractTargets");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreSniper", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.HasKey("UserId");
b.ToTable("ScoresSniper");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreStory", b =>
{
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.HasKey("Id", "UserId");
b.HasIndex("UserId");
b.ToTable("ScoresStory");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreTutorial", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<int>("Score")
.HasColumnType("INTEGER");
b.HasKey("UserId");
b.ToTable("ScoresTutorial");
});
modelBuilder.Entity("Cobra.Server.Database.Models.User", b =>
{
b.Property<ulong>("Id")
.HasColumnType("INTEGER");
b.Property<int>("CompetitionPlays")
.HasColumnType("INTEGER");
b.Property<int>("ContractPlays")
.HasColumnType("INTEGER");
b.Property<int>("Country")
.HasColumnType("INTEGER");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Trophies")
.HasColumnType("INTEGER");
b.Property<int>("Wallet")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Country");
b.ToTable("Users");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserContract", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<uint>("ContractId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastPlayedAt")
.HasColumnType("TEXT");
b.Property<bool?>("Liked")
.HasColumnType("INTEGER");
b.Property<int?>("Plays")
.HasColumnType("INTEGER");
b.Property<bool>("Queued")
.HasColumnType("INTEGER");
b.Property<int?>("Score")
.HasColumnType("INTEGER");
b.HasKey("UserId", "ContractId");
b.HasIndex("ContractId");
b.HasIndex("UserId");
b.HasIndex("UserId", "Queued");
b.ToTable("UserContracts");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserFriend", b =>
{
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<ulong>("SteamId")
.HasColumnType("INTEGER");
b.HasKey("UserId", "SteamId");
b.ToTable("UserFriends");
});
modelBuilder.Entity("Cobra.Server.Database.Models.Contract", b =>
{
b.HasOne("Cobra.Server.Database.Models.ContractTarget", "Target1")
.WithMany()
.HasForeignKey("Target1Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Cobra.Server.Database.Models.ContractTarget", "Target2")
.WithMany()
.HasForeignKey("Target2Id");
b.HasOne("Cobra.Server.Database.Models.ContractTarget", "Target3")
.WithMany()
.HasForeignKey("Target3Id");
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Target1");
b.Navigation("Target2");
b.Navigation("Target3");
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreSniper", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreStory", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.ScoreTutorial", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserContract", b =>
{
b.HasOne("Cobra.Server.Database.Models.Contract", "Contract")
.WithMany()
.HasForeignKey("ContractId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Contract");
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.UserFriend", b =>
{
b.HasOne("Cobra.Server.Database.Models.User", "User")
.WithMany("Friends")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Cobra.Server.Database.Models.User", b =>
{
b.Navigation("Friends");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cobra.Server.Database.Enums;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database.Models
{
[Index(nameof(DisplayId), IsUnique = true)]
public class Contract
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public uint Id { get; set; }
[Required]
public string DisplayId { get; set; }
[Required]
public string Title { get; set; }
[Required]
public string Description { get; set; }
[Required]
public User User { get; set; }
public int LevelIndex { get; set; }
public int CheckpointIndex { get; set; }
public int Difficulty { get; set; }
public int ExitId { get; set; }
public int WeaponToken { get; set; }
public int OutfitToken { get; set; }
[Required]
public ContractTarget Target1 { get; set; }
public ContractTarget Target2 { get; set; }
public ContractTarget Target3 { get; set; }
public EContractRestrictionType Restrictions { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database.Models
{
[Index(
nameof(Name),
nameof(WeaponToken), nameof(OutfitToken), nameof(AmmoType), nameof(SpecialSituation),
IsUnique = true
)]
public class ContractTarget
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public uint Id { get; set; }
[Required]
public string Name { get; set; }
public int WeaponToken { get; set; }
public int OutfitToken { get; set; }
public int AmmoType { get; set; }
public int SpecialSituation { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Cobra.Server.Database.Models
{
public class ScoreSniper
{
[Key, ForeignKey(nameof(User))]
public ulong UserId { get; set; }
public int Score { get; set; }
public User User { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database.Models
{
[PrimaryKey(nameof(Id), nameof(UserId))]
public class ScoreStory
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public uint Id { get; set; } //NOTE: Leaderboard ID
[ForeignKey(nameof(User))]
public ulong UserId { get; set; }
public int Score { get; set; }
public User User { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Cobra.Server.Database.Models
{
public class ScoreTutorial
{
[Key, ForeignKey(nameof(User))]
public ulong UserId { get; set; }
public int Score { get; set; }
public User User { get; set; }
}
}

View File

@ -1,19 +1,24 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database.Models
{
[Index(nameof(Country))]
public class User
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public ulong Id { get; set; }
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public ulong Id { get; set; } //NOTE: SteamId
public ulong SteamId { get; set; }
[Required]
public string DisplayName { get; set; }
public int Country { get; set; }
public int Wallet { get; set; }
public int ContractPlays { get; set; }
public int CompetitionPlays { get; set; }
public int Trophies { get; set; }
public ICollection<UserFriend> Friends { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database.Models
{
[PrimaryKey(nameof(UserId), nameof(ContractId))]
[Index(nameof(UserId))]
[Index(nameof(UserId), nameof(Queued))]
public class UserContract
{
[ForeignKey(nameof(User))]
public ulong UserId { get; set; }
[ForeignKey(nameof(Contract))]
public uint ContractId { get; set; }
public bool Queued { get; set; }
public int? Plays { get; set; }
public int? Score { get; set; }
public bool? Liked { get; set; }
public DateTime? LastPlayedAt { get; set; }
public User User { get; set; }
public Contract Contract { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database.Models
{
[PrimaryKey(nameof(UserId), nameof(SteamId))]
public class UserFriend
{
[ForeignKey(nameof(User))]
public ulong UserId { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public ulong SteamId { get; set; }
public User User { get; set; }
}
}

View File

@ -14,6 +14,7 @@
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Analyzer</OutputItemType>
</ProjectReference>
<ProjectReference Include="..\Cobra.Server.Database\Cobra.Server.Database.csproj" />
<ProjectReference Include="..\Cobra.Server.Shared\Cobra.Server.Shared.csproj" />
<ProjectReference Include="..\Cobra.Server.Edm\Cobra.Server.Edm.csproj" />
</ItemGroup>

View File

@ -0,0 +1,39 @@
using Cobra.Server.Hitman.Interfaces;
using Cobra.Server.Hitman.Services;
using Cobra.Server.Shared.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Cobra.Server.Hitman
{
public static class Compositions
{
public static void ConfigureServices(IServiceCollection services, IConfiguration configuration, Options options)
{
services.AddSingleton<IHitmanMetadataService, HitmanMetadataService>();
switch (options.GameService)
{
case Options.EGameService.Mocked:
services.AddSingleton<IHitmanServer, MockedHitmanServer>();
break;
case Options.EGameService.Local:
services.AddSingleton<IHitmanServer, LocalHitmanServer>();
services.AddSingleton<IContractsService, LocalContractsService>();
break;
case Options.EGameService.Public:
services.AddSingleton<IHitmanServer, HitmanServer>();
services.AddSingleton<IHitmanUserService, HitmanUserService>();
break;
}
}
public static void Configure(IServiceProvider services)
{
var hitmanServer = services.GetRequiredService<IHitmanServer>();
hitmanServer.Initialize();
}
}
}

View File

@ -26,9 +26,9 @@ namespace Cobra.Server.Hitman.Controllers
[HttpGet]
[Route("GetUserOverviewData")]
public IActionResult GetUserOverviewData([FromQuery] GetUserOverviewDataRequest request)
public async Task<IActionResult> GetUserOverviewData([FromQuery] GetUserOverviewDataRequest request)
{
return JsonEntryResponse(_hitmanServer.GetUserOverviewData(request));
return JsonEntryResponse(await _hitmanServer.GetUserOverviewData(request));
}
}
}

View File

@ -15,8 +15,8 @@ namespace Cobra.Server.Hitman.Interfaces
Contract GetFeaturedContract(GetFeaturedContractRequest request);
List<Message> GetMessages(GetMessagesRequest request);
int GetNewMessageCount(GetNewMessageCountRequest request);
GetUserOverviewData GetUserOverviewData(GetUserOverviewDataRequest request);
int GetUserWallet(GetUserWalletRequest request);
Task<GetUserOverviewData> GetUserOverviewData(GetUserOverviewDataRequest request);
Task<int> GetUserWallet(GetUserWalletRequest request);
void InviteToCompetition(InviteToCompetitionRequest request);
void MarkContractAsPlayed(MarkContractAsPlayedRequest request);
//MergeUserTokens
@ -29,7 +29,7 @@ namespace Cobra.Server.Hitman.Interfaces
void SetMessageReadStatus(SetMessageReadStatusRequest request);
void UpdateContractLikeDislikes(UpdateContractLikeDislikesRequest request);
//UpdateDLCInfo
void UpdateUserInfo(UpdateUserInfoRequest request);
Task UpdateUserInfo(UpdateUserInfoRequest request);
//UpdateUserProfileChallenges
//UpdateUserProfileGameStats
//UpdateUserProfileLevelProgression

View File

@ -0,0 +1,11 @@
using Cobra.Server.Hitman.Models;
namespace Cobra.Server.Hitman.Interfaces
{
public interface IHitmanUserService
{
Task<GetUserOverviewData> GetUserOverviewData(ulong userId);
Task<int?> GetUserWallet(ulong userId);
Task UpdateUserInfo(ulong userId, string displayName, int country, List<ulong> friends);
}
}

View File

@ -0,0 +1,156 @@
using Cobra.Server.Hitman.Controllers;
using Cobra.Server.Hitman.Interfaces;
using Cobra.Server.Hitman.Models;
using Cobra.Server.Shared.Interfaces;
namespace Cobra.Server.Hitman.Services
{
public class HitmanServer : IHitmanServer
{
private readonly IUserService _userService;
private readonly IHitmanUserService _hitmanUserService;
public HitmanServer(
IUserService userService,
IHitmanUserService hitmanUserService
)
{
_userService = userService;
_hitmanUserService = hitmanUserService;
}
public void Initialize()
{
//Do nothing
}
public void CreateCompetition(HitmanController.CreateCompetitionRequest request)
{
//Do nothing
}
public int ExecuteWalletTransaction(HitmanController.ExecuteWalletTransactionRequest request)
{
return 0;
}
public List<int> GetAverageScores(HitmanController.BaseGetAverageScoresRequest request)
{
return new List<int> { 0, 0, 0, 0 };
}
public List<ScoreEntry> GetScores(HitmanController.BaseGetScoresRequest request)
{
return new List<ScoreEntry>();
}
public ScoreComparison GetScoreComparison(HitmanController.GetScoreComparisonRequest request)
{
return null;
}
public Contract GetFeaturedContract(HitmanController.GetFeaturedContractRequest request)
{
return null;
}
public List<Message> GetMessages(HitmanController.GetMessagesRequest request)
{
return new List<Message>();
}
public int GetNewMessageCount(HitmanController.GetNewMessageCountRequest request)
{
return 0;
}
public async Task<GetUserOverviewData> GetUserOverviewData(HitmanController.GetUserOverviewDataRequest request)
{
var userId = _userService.GetCurrentUserId();
return await _hitmanUserService.GetUserOverviewData(userId);
}
public async Task<int> GetUserWallet(HitmanController.GetUserWalletRequest request)
{
var userId = _userService.GetCurrentUserId();
return await _hitmanUserService.GetUserWallet(userId) ?? 0;
}
public void InviteToCompetition(HitmanController.InviteToCompetitionRequest request)
{
//Do nothing
}
public void MarkContractAsPlayed(HitmanController.MarkContractAsPlayedRequest request)
{
//Do nothing
}
public int PutScore(HitmanController.PutScoreRequest request)
{
return 0;
}
public void QueueAddContract(HitmanController.QueueAddContractRequest request)
{
//Do nothing
}
public void QueueRemoveContract(HitmanController.QueueRemoveContractRequest request)
{
//Do nothing
}
public void ReportContract(HitmanController.ReportContractRequest request)
{
//Do nothing
}
public List<Contract> SearchForContracts2(HitmanController.SearchForContracts2Request request)
{
return new List<Contract>();
}
public void SendTemplatedMessage(HitmanController.SendTemplatedMessageRequest request)
{
//Do nothing
}
public void SetMessageReadStatus(HitmanController.SetMessageReadStatusRequest request)
{
//Do nothing
}
public void UpdateContractLikeDislikes(HitmanController.UpdateContractLikeDislikesRequest request)
{
//Do nothing
}
public Task UpdateUserInfo(HitmanController.UpdateUserInfoRequest request)
{
var userId = _userService.GetCurrentUserId();
var friends = request.Friends
.Select(ulong.Parse)
.ToList();
//TODO: Validate if country is a valid id, this is a non-standard and will have to be extracted from the game.
_hitmanUserService.UpdateUserInfo(
userId,
request.DisplayName,
request.Country,
friends
);
return Task.CompletedTask;
}
public void UploadContract(HitmanController.UploadContractRequest request)
{
//Do nothing
}
}
}

View File

@ -0,0 +1,79 @@
using Cobra.Server.Database;
using Cobra.Server.Database.Models;
using Cobra.Server.Hitman.Interfaces;
using Cobra.Server.Hitman.Models;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Hitman.Services
{
public class HitmanUserService : IHitmanUserService
{
private readonly IDbContextFactory<DatabaseContext> _databaseContextFactory;
public HitmanUserService(IDbContextFactory<DatabaseContext> databaseContextFactory)
{
_databaseContextFactory = databaseContextFactory;
}
public async Task<GetUserOverviewData> GetUserOverviewData(ulong userId)
{
await using var databaseContext = await _databaseContextFactory.CreateDbContextAsync();
var user = await databaseContext.Users.FindAsync(userId);
if (user == null)
{
return null;
}
//TODO: Perform a specific (agnostic) query to satisfy as much data as possible
return new GetUserOverviewData
{
WalletAmount = user.Wallet,
ContractPlays = user.ContractPlays,
CompetitionPlays = user.CompetitionPlays,
TrophiesEarned = user.Trophies
};
}
public async Task<int?> GetUserWallet(ulong userId)
{
await using var databaseContext = await _databaseContextFactory.CreateDbContextAsync();
var user = await databaseContext.Users.FindAsync(userId);
return user?.Wallet;
}
public async Task UpdateUserInfo(ulong userId, string displayName, int country, List<ulong> friends)
{
await using var databaseContext = await _databaseContextFactory.CreateDbContextAsync();
var user = await databaseContext.Users
.Include(x => x.Friends)
.FirstOrDefaultAsync(x => x.Id == userId);
if (user == null)
{
user = new User
{
Id = userId
};
databaseContext.Users.Add(user);
}
user.DisplayName = displayName;
user.Country = country;
//NOTE: This will effectively drop old friends and add new ones
user.Friends = friends.Select(x => new UserFriend
{
User = user,
SteamId = x
}).ToList();
await databaseContext.SaveChangesAsync();
}
}
}

View File

@ -11,7 +11,7 @@ using Microsoft.AspNetCore.WebUtilities;
namespace Cobra.Server.Hitman.Services
{
public class ContractsService : IContractsService
public class LocalContractsService : IContractsService
{
public class SimpleContract
{
@ -33,7 +33,7 @@ namespace Cobra.Server.Hitman.Services
private readonly ConcurrentDictionary<string, Contract> _contractCache;
public ContractsService()
public LocalContractsService()
{
_contractCache = new ConcurrentDictionary<string, Contract>();
}

View File

@ -52,7 +52,10 @@ namespace Cobra.Server.Hitman.Services
private UserProfile _userProfile;
public LocalHitmanServer(ISimpleLogger logger, IContractsService contractsService)
public LocalHitmanServer(
ISimpleLogger logger,
IContractsService contractsService
)
{
_logger = logger;
_contractsService = contractsService;
@ -124,19 +127,19 @@ namespace Cobra.Server.Hitman.Services
return 0;
}
public GetUserOverviewData GetUserOverviewData(GetUserOverviewDataRequest request)
public Task<GetUserOverviewData> GetUserOverviewData(GetUserOverviewDataRequest request)
{
return new GetUserOverviewData
return Task.FromResult(new GetUserOverviewData
{
WalletAmount = _userProfile.TotalEarnings,
ContractPlays = _userProfile.PlayedContracts.Sum(x => x.Value.Plays),
ContractsCreated = _userProfile.ContractsCreated
};
});
}
public int GetUserWallet(GetUserWalletRequest request)
public Task<int> GetUserWallet(GetUserWalletRequest request)
{
return _userProfile.WalletAmount;
return Task.FromResult(_userProfile.WalletAmount);
}
public void InviteToCompetition(InviteToCompetitionRequest request)
@ -240,7 +243,7 @@ namespace Cobra.Server.Hitman.Services
//Do nothing
}
public void UpdateUserInfo(UpdateUserInfoRequest request)
public Task UpdateUserInfo(UpdateUserInfoRequest request)
{
SaveUserProfile(() =>
{
@ -249,6 +252,8 @@ namespace Cobra.Server.Hitman.Services
request.Friends.ForEach(x => _userProfile.Friends.Add(x));
});
return Task.CompletedTask;
}
public void UploadContract(UploadContractRequest request)

View File

@ -287,9 +287,9 @@ namespace Cobra.Server.Hitman.Services
return 10;
}
public GetUserOverviewData GetUserOverviewData(GetUserOverviewDataRequest request)
public Task<GetUserOverviewData> GetUserOverviewData(GetUserOverviewDataRequest request)
{
return new GetUserOverviewData
return Task.FromResult(new GetUserOverviewData
{
ContractPlays = 1337,
CompetitionPlays = 1337,
@ -303,12 +303,12 @@ namespace Cobra.Server.Hitman.Services
RichestRank = 1337,
TrophiesEarned = 1337,
WalletAmount = _options.WalletAmount
};
});
}
public int GetUserWallet(GetUserWalletRequest request)
public Task<int> GetUserWallet(GetUserWalletRequest request)
{
return _options.WalletAmount;
return Task.FromResult(_options.WalletAmount);
}
public void InviteToCompetition(InviteToCompetitionRequest request)
@ -365,9 +365,9 @@ namespace Cobra.Server.Hitman.Services
//Do nothing
}
public void UpdateUserInfo(UpdateUserInfoRequest request)
public Task UpdateUserInfo(UpdateUserInfoRequest request)
{
//Do nothing
return Task.CompletedTask;
}
public void UploadContract(UploadContractRequest request)

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
@ -9,7 +9,6 @@
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\Cobra.Analyzer\Cobra.Analyzer.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Analyzer</OutputItemType>

View File

@ -0,0 +1,7 @@
namespace Cobra.Server.Shared.Interfaces
{
public interface IUserService
{
public ulong GetCurrentUserId();
}
}

View File

@ -2,6 +2,13 @@
{
public class Options
{
public enum EGameService
{
Mocked = 0,
Local,
Public
}
public enum ESteamService
{
None = 0,
@ -15,7 +22,7 @@
public bool EnableResponseBodyLogging { get; set; } = false;
public string MockedContractSteamId { get; set; } = "76561198161220058";
public int WalletAmount { get; set; } = 1337;
public bool UseCustomContracts { get; set; } = false;
public EGameService GameService { get; set; } = EGameService.Mocked;
public int JwtTokenExpirationInSeconds { get; set; } = 60 * 60 * 8; //NOTE: 8 hours
public string JwtSignKey { get; set; } = Guid.NewGuid().ToString();
public ESteamService SteamService { get; set; } = ESteamService.None;

View File

@ -0,0 +1,22 @@
using Cobra.Server.Shared.Models;
using Cobra.Server.Sniper.Interfaces;
using Cobra.Server.Sniper.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Cobra.Server.Sniper
{
public static class Compositions
{
public static void ConfigureServices(IServiceCollection services, IConfiguration configuration, Options options)
{
services.AddSingleton<ISniperMetadataService, SniperMetadataService>();
services.AddSingleton<ISniperServer, MockedSniperServer>();
}
public static void Configure(IServiceProvider services)
{
//Do nothing
}
}
}

View File

@ -26,6 +26,7 @@
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Analyzer</OutputItemType>
</ProjectReference>
<ProjectReference Include="..\Cobra.Server.Database\Cobra.Server.Database.csproj" />
<ProjectReference Include="..\Cobra.Server.Shared\Cobra.Server.Shared.csproj" />
<ProjectReference Include="..\Cobra.Server.Hitman\Cobra.Server.Hitman.csproj" />
<ProjectReference Include="..\Cobra.Server.Sniper\Cobra.Server.Sniper.csproj" />

View File

@ -1,16 +0,0 @@
using Cobra.Server.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Cobra.Server.Database
{
public class DatabaseContext : DbContext
{
public DbSet<User> Users { get; set; }
public DatabaseContext(DbContextOptions<DatabaseContext> dbContextOptions)
: base(dbContextOptions)
{
//Do nothing
}
}
}

View File

@ -1 +0,0 @@


View File

@ -1,56 +0,0 @@
// <auto-generated />
using Cobra.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cobra.Server.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20231022225634_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.12");
modelBuilder.Entity("Cobra.Server.Database.Models.User", b =>
{
b.Property<ulong>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CompetitionPlays")
.HasColumnType("INTEGER");
b.Property<int>("ContractPlays")
.HasColumnType("INTEGER");
b.Property<int>("Country")
.HasColumnType("INTEGER");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<ulong>("SteamId")
.HasColumnType("INTEGER");
b.Property<int>("Trophies")
.HasColumnType("INTEGER");
b.Property<int>("Wallet")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -1,40 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cobra.Server.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
SteamId = table.Column<ulong>(type: "INTEGER", nullable: false),
DisplayName = table.Column<string>(type: "TEXT", nullable: true),
Country = table.Column<int>(type: "INTEGER", nullable: false),
Wallet = table.Column<int>(type: "INTEGER", nullable: false),
ContractPlays = table.Column<int>(type: "INTEGER", nullable: false),
CompetitionPlays = table.Column<int>(type: "INTEGER", nullable: false),
Trophies = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View File

@ -1,53 +0,0 @@
// <auto-generated />
using Cobra.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Cobra.Server.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.12");
modelBuilder.Entity("Cobra.Server.Database.Models.User", b =>
{
b.Property<ulong>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CompetitionPlays")
.HasColumnType("INTEGER");
b.Property<int>("ContractPlays")
.HasColumnType("INTEGER");
b.Property<int>("Country")
.HasColumnType("INTEGER");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<ulong>("SteamId")
.HasColumnType("INTEGER");
b.Property<int>("Trophies")
.HasColumnType("INTEGER");
b.Property<int>("Wallet")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,16 @@
using System.Security.Claims;
namespace Cobra.Server.Models
{
public class CustomIdentity : ClaimsIdentity
{
public override bool IsAuthenticated => true;
public ulong SteamId { get; init; }
public CustomIdentity(ulong steamId)
{
SteamId = steamId;
}
}
}

View File

@ -1,23 +1,25 @@
using System.Security.Cryptography;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Cobra.Server.Interfaces;
using Cobra.Server.Models;
using Cobra.Server.Shared.Models;
namespace Cobra.Server.Mvc
{
public class SteamAuthMiddleware : IMiddleware
{
private class JwtToken
private sealed class JwtToken
{
public class JwtTokenPayload
{
public ulong Uid { get; set; }
public long Timestamp { get; set; }
public ulong Uid { get; init; }
public long Timestamp { get; init; }
}
public JwtTokenPayload Payload { get; set; }
public string Hash { get; set; }
public JwtTokenPayload Payload { get; init; }
public string Hash { get; init; }
}
private const string REQUEST_HEADER_OSAUTHPROVIDER = "OS-AuthProvider";
@ -45,11 +47,12 @@ namespace Cobra.Server.Mvc
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
//TODO: Move this middleware to a filter instead?
if (
context.Request.Path is { HasValue: true, Value: not null } &&
!context.Request.Path.Value.StartsWith("/hm5") &&
!context.Request.Path.Value.StartsWith("/sniper")
!context.Request.Path.HasValue ||
(
!context.Request.Path.Value.StartsWith("/hm5") &&
!context.Request.Path.Value.StartsWith("/sniper")
)
)
{
await next(context);
@ -58,35 +61,49 @@ namespace Cobra.Server.Mvc
}
var authProvider = context.Request.Headers[REQUEST_HEADER_OSAUTHPROVIDER];
var authTicketData = context.Request.Headers[REQUEST_HEADER_OSAUTHTICKETDATA];
var uid = context.Request.Headers[REQUEST_HEADER_OSUID];
//NOTE: Check for our own AuthProvider first
if (authProvider == OSAUTHPROVIDER_SERVER)
if (!TryDecodeBase64String(context.Request.Headers[REQUEST_HEADER_OSAUTHTICKETDATA], out var authTicketData))
{
var jwtToken = DecodeSimpleJwtToken(authTicketData);
if (
jwtToken != null &&
jwtToken.Uid == ulong.Parse(uid) &&
DateTimeOffset.Now.ToUnixTimeSeconds() - jwtToken.Timestamp < _jwtTokenExpirationInSeconds
)
{
await next(context);
}
RejectRequest(context, OSERROR_EXPIRED);
RejectRequest(context, OSERROR_FAILED);
return;
}
//NOTE: If we didn't pass the previous check, enforce valid AuthTicketData.
if (!ulong.TryParse(context.Request.Headers[REQUEST_HEADER_OSUID], out var steamId))
{
RejectRequest(context, OSERROR_FAILED);
return;
}
//NOTE: Wrap in a try-catch to make sure unhandled situations result in a rejected response
try
{
var authTicketDataBytes = Convert.FromBase64String(authTicketData);
var steamId = ulong.Parse(uid);
//NOTE: Check for our own AuthProvider first
if (authProvider == OSAUTHPROVIDER_SERVER)
{
var jwtToken = DecodeSimpleJwtToken(authTicketData);
var result = await _steamService.AuthenticateUser(authTicketDataBytes, steamId);
if (
jwtToken != null &&
jwtToken.Uid == steamId &&
DateTimeOffset.Now.ToUnixTimeSeconds() - jwtToken.Timestamp < _jwtTokenExpirationInSeconds
)
{
AuthenticateRequest(context, jwtToken.Uid);
await next(context);
return;
}
RejectRequest(context, OSERROR_EXPIRED);
return;
}
//NOTE: If we didn't pass the previous check, enforce valid AuthTicketData.
var result = await _steamService.AuthenticateUser(authTicketData, steamId);
if (result)
{
@ -98,6 +115,8 @@ namespace Cobra.Server.Mvc
context.Response.Headers[RESPONSE_HEADER_OSAUTHRESPONSE] = jwtToken;
AuthenticateRequest(context, steamId);
await next(context);
return;
@ -112,12 +131,43 @@ namespace Cobra.Server.Mvc
RejectRequest(context, OSERROR_FAILED);
}
private static void AuthenticateRequest(HttpContext context, ulong steamId)
{
context.User = new ClaimsPrincipal(new CustomIdentity(steamId));
}
private static void RejectRequest(HttpContext context, int osError)
{
context.Response.StatusCode = 403;
context.Response.Headers[RESPONSE_HEADER_OSERROR] = osError.ToString();
}
private static bool TryDecodeBase64String(string base64String, out byte[] bytes)
{
if (base64String == null)
{
bytes = null;
return false;
}
Span<byte> bytesBuffer = stackalloc byte[base64String.Length];
if (
!Convert.TryFromBase64String(base64String, bytesBuffer, out var bytesWritten) ||
bytesWritten == 0
)
{
bytes = null;
return false;
}
bytes = bytesBuffer[..bytesWritten].ToArray();
return true;
}
private string EncodeSimpleJwtToken(JwtToken.JwtTokenPayload payload)
{
using var hasher = new HMACSHA256(_jwtSignKey);
@ -142,12 +192,10 @@ namespace Cobra.Server.Mvc
);
}
private JwtToken.JwtTokenPayload DecodeSimpleJwtToken(string simpleJwtToken)
private JwtToken.JwtTokenPayload DecodeSimpleJwtToken(byte[] simpleJwtToken)
{
var jwtToken = JsonSerializer.Deserialize<JwtToken>(
Encoding.UTF8.GetString(
Convert.FromBase64String(simpleJwtToken)
)
Encoding.UTF8.GetString(simpleJwtToken)
);
using var hasher = new HMACSHA256(_jwtSignKey);

View File

@ -0,0 +1,22 @@
using Cobra.Server.Models;
using Cobra.Server.Shared.Interfaces;
namespace Cobra.Server.Services
{
public class UserService : IUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserService(
IHttpContextAccessor httpContextAccessor
)
{
_httpContextAccessor = httpContextAccessor;
}
public ulong GetCurrentUserId()
{
return (_httpContextAccessor.HttpContext?.User.Identity as CustomIdentity)?.SteamId ?? 0UL;
}
}
}

View File

@ -1,17 +1,14 @@
using System.Text.Encodings.Web;
using System.Text.Json.Serialization;
using Cobra.Server.Database;
using Cobra.Server.Edm.Json;
using Cobra.Server.Hitman.Interfaces;
using Cobra.Server.Hitman.Services;
using Cobra.Server.Interfaces;
using Cobra.Server.Mvc;
using Cobra.Server.Services;
using Cobra.Server.Shared.Interfaces;
using Cobra.Server.Shared.Models;
using Cobra.Server.Sniper.Interfaces;
using Cobra.Server.Sniper.Services;
using Microsoft.EntityFrameworkCore;
using DatabaseCompositions = Cobra.Server.Database.Compositions;
using HitmanCompositions = Cobra.Server.Hitman.Compositions;
using SniperCompositions = Cobra.Server.Sniper.Compositions;
namespace Cobra.Server
{
@ -53,61 +50,48 @@ namespace Cobra.Server
options.JsonSerializerOptions.Converters.Add(new IntegerToStringConverter());
});
services.AddHttpContextAccessor();
var options = _configuration
.GetSection("Options")
.Get<Options>();
services.AddSingleton(options);
services.AddDbContext<DatabaseContext>(optionsBuilder =>
{
optionsBuilder.UseSqlite(_configuration.GetConnectionString("CobraDatabase"));
});
//Shared
services.AddSingleton<ISimpleLogger>(_ => new SimpleLogger("Data"));
services.AddSingleton<IUserService, UserService>();
services.AddSingleton<IHitmanMetadataService, HitmanMetadataService>();
services.AddSingleton<ISniperMetadataService, SniperMetadataService>();
switch (options.SteamService)
{
case Options.ESteamService.GameServer:
services.AddSingleton<ISteamService, SteamGameServerService>();
break;
case Options.ESteamService.WebApi:
services.AddSingleton<ISteamService, SteamWebApiService>();
break;
}
//Middleware
services.AddTransient<FixAddMetricsContentTypeMiddleware>();
services.AddTransient<RequestResponseLoggerMiddleware>();
services.AddTransient<SteamAuthMiddleware>();
services.AddSingleton<IContractsService, ContractsService>();
if (options.UseCustomContracts)
{
services.AddSingleton<IHitmanServer, LocalHitmanServer>();
}
else
{
services.AddSingleton<IHitmanServer, MockedHitmanServer>();
}
services.AddSingleton<ISniperServer, MockedSniperServer>();
if (options.SteamService == Options.ESteamService.GameServer)
{
services.AddSingleton<ISteamService, SteamGameServerService>();
}
else if (options.SteamService == Options.ESteamService.WebApi)
{
services.AddSingleton<ISteamService, SteamWebApiService>();
}
//Compositions
DatabaseCompositions.ConfigureServices(services, _configuration, options);
HitmanCompositions.ConfigureServices(services, _configuration, options);
SniperCompositions.ConfigureServices(services, _configuration, options);
}
public void Configure(
IApplicationBuilder app,
Options options,
IHitmanServer hitmanServer,
ISniperServer sniperServer,
DatabaseContext databaseContext
IServiceProvider services,
Options options
)
{
databaseContext.Database.Migrate();
hitmanServer.Initialize();
sniperServer.Initialize();
DatabaseCompositions.Configure(services);
HitmanCompositions.Configure(services);
SniperCompositions.Configure(services);
if (options.FixAddMetricsContentType)
{

View File

@ -14,8 +14,7 @@
"EnableRequestLogging": true,
"EnableRequestBodyLogging": false,
"EnableResponseBodyLogging": false,
"MockedContractSteamId": "76561198161220058",
"WalletAmount": 1337,
"UseCustomContracts": true
"GameService": "Public",
"SteamService": "GameServer"
}
}

View File

@ -7,10 +7,12 @@
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
<NoWarn>JSON002</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -29,6 +31,7 @@
</ProjectReference>
<ProjectReference Include="..\Cobra.Server.Edm\Cobra.Server.Edm.csproj" />
<ProjectReference Include="..\Cobra.Server.Hitman\Cobra.Server.Hitman.csproj" />
<ProjectReference Include="..\Cobra.Server\Cobra.Server.csproj" />
</ItemGroup>
</Project>

View File

@ -6,9 +6,9 @@ using Cobra.Server.Edm.Interfaces;
using Cobra.Server.Edm.Json;
using Microsoft.AspNetCore.Mvc;
namespace Cobra.Test
namespace Cobra.Test.Edm
{
public class BaseControllerTests
public class BaseEdmControllerTests
{
public class TestClass
{
@ -75,7 +75,7 @@ namespace Cobra.Test
private readonly TestController _testController;
public BaseControllerTests()
public BaseEdmControllerTests()
{
_testController = new TestController();
}

View File

@ -5,7 +5,7 @@ using Cobra.Server.Edm.Json;
using Cobra.Server.Hitman.Models;
using Nullable = Cobra.Server.Edm.Enums.Nullable;
namespace Cobra.Test
namespace Cobra.Test.Edm
{
public class JsonConverterTests
{

View File

@ -3,8 +3,9 @@ using Cobra.Server.Edm.Enums;
using Cobra.Server.Edm.Interfaces;
using Cobra.Server.Edm.Models.Base;
using Cobra.Server.Edm.Services;
using System.Diagnostics.CodeAnalysis;
namespace Cobra.Test
namespace Cobra.Test.Edm
{
public class MetadataServiceTests
{
@ -29,6 +30,7 @@ namespace Cobra.Test
protected override List<Type> GetEdmFunctionImports() => _edmFunctionImports;
}
[ExcludeFromCodeCoverage]
[EdmEntity("EntityTest")]
public class TestEntityValid : IEdmEntity
{
@ -52,6 +54,7 @@ namespace Cobra.Test
//Do nothing
}
[ExcludeFromCodeCoverage]
[EdmFunctionImport("FunctionImportTest", HttpMethods.GET, "Test.EntityTest")]
public class TestFunctionImportValid : IEdmFunctionImport
{

View File

@ -1,4 +1,5 @@
using System.Globalization;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json.Serialization;
using Cobra.Server.Edm.Mvc;
using Microsoft.AspNetCore.Http;
@ -6,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Primitives;
namespace Cobra.Test
namespace Cobra.Test.Edm
{
public class ModelBinderTests
{
@ -16,6 +17,7 @@ namespace Cobra.Test
}
//ReSharper disable UnassignedGetOnlyAutoProperty
[ExcludeFromCodeCoverage]
public class TestModelMetadata : ModelMetadata
{
public override IReadOnlyDictionary<object, object> AdditionalValues { get; }

View File

@ -0,0 +1,14 @@
using System.Linq.Expressions;
using Moq;
namespace Cobra.Test.Extensions
{
public static class MoqExtensions
{
public static Expression<Func<T, TResult>> Expression<T, TResult>(this Mock<T> mock, Expression<Func<T, TResult>> expression)
where T : class
{
return expression;
}
}
}

View File

@ -0,0 +1,11 @@
namespace Cobra.Test.Hitman
{
public class TempTests
{
[Fact]
public void Test()
{
Assert.True(true);
}
}
}

View File

@ -0,0 +1,345 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Cobra.Server.Interfaces;
using Cobra.Server.Models;
using Cobra.Server.Mvc;
using Cobra.Server.Shared.Models;
using Cobra.Test.Extensions;
using Microsoft.AspNetCore.Http;
using Moq;
namespace Cobra.Test.Server
{
public class SteamAuthMiddlewareTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("/")]
[InlineData("/test")]
[InlineData("/unknown")]
public async Task PathWithUnknownPrefix_ShouldBe_Skipped(string path)
{
var (_, steamAuthMiddleware, httpContext) = SetupInstances(path);
HttpContext nextContext = null;
await steamAuthMiddleware.InvokeAsync(httpContext, context =>
{
nextContext = context;
return Task.CompletedTask;
});
Assert.NotNull(nextContext);
Assert.Equal(httpContext, nextContext);
Assert.False(httpContext.User.Identity?.IsAuthenticated);
Assert.Equal(200, httpContext.Response.StatusCode);
Assert.False(httpContext.Response.Headers.ContainsKey("OS-Error"));
}
[Theory]
[InlineData(null, 47)]
[InlineData("unknown", 47)]
[InlineData("unknown", 101)]
[InlineData("unknown", 1337)]
public async Task UnknownAuthProviderWithValidAuthentication_ShouldBe_Authenticated(string authProvider, ulong userId)
{
var (steamServiceMock, steamAuthMiddleware, httpContext) = SetupInstances(
"/hm5",
authProvider: authProvider,
authTicketData: Convert.ToBase64String(Encoding.UTF8.GetBytes(userId.ToString())),
uid: userId.ToString()
);
var steamServiceExpression = steamServiceMock.Expression(x => x.AuthenticateUser(
It.IsAny<byte[]>(),
It.IsAny<ulong>()
));
steamServiceMock.Setup(steamServiceExpression).ReturnsAsync(true);
HttpContext nextContext = null;
await steamAuthMiddleware.InvokeAsync(httpContext, context =>
{
nextContext = context;
return Task.CompletedTask;
});
steamServiceMock.Verify(steamServiceExpression, Times.Once);
Assert.NotNull(nextContext);
Assert.Equal(httpContext, nextContext);
Assert.True(httpContext.User.Identity!.IsAuthenticated);
Assert.Equal(userId, ((CustomIdentity)httpContext.User.Identity).SteamId);
Assert.True(httpContext.Response.Headers.ContainsKey("OS-AuthResponse"));
Assert.Equal(200, httpContext.Response.StatusCode);
Assert.False(httpContext.Response.Headers.ContainsKey("OS-Error"));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("invalid")]
public async Task UnknownAuthProviderWithInvalidAuthTicketData_ShouldBe_Rejected(string authTicketData)
{
await TestForRejectedUnknownAuthProvider(
"47",
authTicketData
);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("invalid")]
public async Task UnknownAuthProviderWithInvalidUserId_ShouldBe_Rejected(string userId)
{
await TestForRejectedUnknownAuthProvider(
userId,
Convert.ToBase64String(Encoding.UTF8.GetBytes("47"))
);
}
[Theory]
[InlineData(47)]
[InlineData(101)]
[InlineData(1337)]
public async Task UnknownAuthProviderWithAuthenticationForOtherUser_ShouldBe_Rejected(ulong userId)
{
await TestForRejectedUnknownAuthProvider(
userId.ToString(),
Convert.ToBase64String(
Encoding.UTF8.GetBytes((userId + 1).ToString())
)
);
}
[Theory]
[InlineData(47)]
[InlineData(101)]
[InlineData(1337)]
public async Task KnownAuthProviderWithValidAuthentication_ShouldBe_Authenticated(ulong userId)
{
var authTicketData = await GetAuthTicketDataForUserId(userId);
var (_, steamAuthMiddleware, httpContext) = SetupInstances(
"/hm5",
authProvider: "6",
authTicketData: authTicketData,
uid: userId.ToString(),
jwtTokenExpirationInSeconds: int.MaxValue
);
HttpContext nextContext = null;
await steamAuthMiddleware.InvokeAsync(httpContext, context =>
{
nextContext = context;
return Task.CompletedTask;
});
Assert.NotNull(nextContext);
Assert.Equal(httpContext, nextContext);
Assert.True(httpContext.User.Identity!.IsAuthenticated);
Assert.Equal(userId, ((CustomIdentity)httpContext.User.Identity).SteamId);
Assert.False(httpContext.Response.Headers.ContainsKey("OS-AuthResponse"));
Assert.Equal(200, httpContext.Response.StatusCode);
Assert.False(httpContext.Response.Headers.ContainsKey("OS-Error"));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("invalid")]
public async Task KnownAuthProviderWithInvalidAuthTicketData_ShouldBe_Rejected(string authTicketData)
{
await TestForRejectedKnownAuthProvider(
"47",
authTicketData,
int.MaxValue,
"1"
);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("invalid")]
public async Task KnownAuthProviderWithInvalidUserId_ShouldBe_Rejected(string userId)
{
var authTicketData = await GetAuthTicketDataForUserId(47);
await TestForRejectedKnownAuthProvider(
userId,
authTicketData,
int.MaxValue,
"1"
);
}
[Theory]
[InlineData(47)]
[InlineData(101)]
[InlineData(1337)]
public async Task KnownAuthProviderWithAuthenticationForOtherUser_ShouldBe_Rejected(ulong userId)
{
var authTicketData = await GetAuthTicketDataForUserId(userId + 1);
//NOTE: We could argue that a user mismatch should be error code 1 instead
await TestForRejectedKnownAuthProvider(
userId.ToString(),
authTicketData,
int.MaxValue,
"2"
);
}
[Theory]
[InlineData(47)]
[InlineData(101)]
[InlineData(1337)]
public async Task KnownAuthProviderWithExpiredAuthentication_ShouldBe_Rejected(ulong userId)
{
var authTicketData = await GetAuthTicketDataForUserId(userId);
await TestForRejectedKnownAuthProvider(
userId.ToString(),
authTicketData,
0,
"2"
);
}
private static (
Mock<ISteamService> steamServiceMock,
SteamAuthMiddleware steamAuthMiddleware,
DefaultHttpContext httpContext
) SetupInstances(
string path,
string authProvider = "",
string authTicketData = "",
string uid = "",
int jwtTokenExpirationInSeconds = 0,
string jwtSignKey = "test"
)
{
var steamServiceMock = new Mock<ISteamService>();
var steamAuthMiddleware = new SteamAuthMiddleware(
steamServiceMock.Object,
new Options
{
JwtTokenExpirationInSeconds = jwtTokenExpirationInSeconds,
JwtSignKey = jwtSignKey
}
);
var httpContext = new DefaultHttpContext
{
Request =
{
Path = path,
Headers =
{
{ "OS-AuthProvider", authProvider},
{ "OS-AuthTicketData", authTicketData},
{ "OS-UID", uid}
}
}
};
return (steamServiceMock, steamAuthMiddleware, httpContext);
}
private static async Task TestForRejectedUnknownAuthProvider(
string userId,
string authTicketData
)
{
var (steamServiceMock, steamAuthMiddleware, httpContext) = SetupInstances(
"/hm5",
authProvider: "unknown",
uid: userId,
authTicketData: authTicketData
);
var steamServiceExpression = steamServiceMock.Expression(x => x.AuthenticateUser(
It.IsAny<byte[]>(),
It.IsAny<ulong>()
));
steamServiceMock.Setup(steamServiceExpression).ReturnsAsync(false);
HttpContext nextContext = null;
await steamAuthMiddleware.InvokeAsync(httpContext, [ExcludeFromCodeCoverage] (context) =>
{
nextContext = context;
return Task.CompletedTask;
});
steamServiceMock.Verify(steamServiceExpression, Times.AtMost(1));
Assert.Null(nextContext);
Assert.False(httpContext.User.Identity?.IsAuthenticated);
Assert.Equal(403, httpContext.Response.StatusCode);
Assert.Equal("1", httpContext.Response.Headers["OS-Error"]);
}
private static async Task TestForRejectedKnownAuthProvider(
string userId,
string authTicketData,
int jwtTokenExpirationInSeconds,
string expectedErrorCode
)
{
var (_, steamAuthMiddleware, httpContext) = SetupInstances(
"/hm5",
authProvider: "6",
authTicketData: authTicketData,
uid: userId,
jwtTokenExpirationInSeconds: jwtTokenExpirationInSeconds
);
HttpContext nextContext = null;
await steamAuthMiddleware.InvokeAsync(httpContext, [ExcludeFromCodeCoverage] (context) =>
{
nextContext = context;
return Task.CompletedTask;
});
Assert.Null(nextContext);
Assert.False(httpContext.User.Identity?.IsAuthenticated);
Assert.Equal(403, httpContext.Response.StatusCode);
Assert.Equal(expectedErrorCode, httpContext.Response.Headers["OS-Error"]);
}
//TODO: Maybe add a service for the encoding/decoding of tokens, so it can be independently used/tested/mocked?
private static async Task<string> GetAuthTicketDataForUserId(ulong userId)
{
var (steamServiceMock, steamAuthMiddleware, httpContext) = SetupInstances(
"/hm5",
authTicketData: Convert.ToBase64String(Encoding.UTF8.GetBytes(userId.ToString())),
uid: userId.ToString()
);
var steamServiceExpression = steamServiceMock.Expression(x => x.AuthenticateUser(
It.IsAny<byte[]>(),
It.IsAny<ulong>()
));
steamServiceMock.Setup(steamServiceExpression).ReturnsAsync(true);
await steamAuthMiddleware.InvokeAsync(httpContext, _ => Task.CompletedTask);
return httpContext.Response.Headers["OS-AuthResponse"].Single();
}
}
}

View File

@ -0,0 +1,11 @@
namespace Cobra.Test.Sniper
{
public class TempTests
{
[Fact]
public void Test()
{
Assert.True(true);
}
}
}