From c5a7844b07ed1b6554bdefe96aa0f918167fa5e8 Mon Sep 17 00:00:00 2001 From: Nickolas Gupton Date: Thu, 23 Nov 2023 04:39:30 -0600 Subject: [PATCH] Cleanup of security scanning services --- .../AssemblyTypeChecker.Config.cs | 159 --- .../ContentScanning/AssemblyTypeChecker.cs | 1243 ----------------- .../AssemblyTypeCheckerHelpers.cs | 447 ++++++ .../ContentScanning/FileInfoComparer.cs | 19 + .../ContentScanning/MetaMembersHelper.cs | 85 -- .../ContentScanning/Parsers.cs | 18 +- .../ContentScanning/Resolver.cs | 94 ++ .../ContentScanning/SandboxError.cs | 17 + .../ContentScanning/ScanningTypes.cs | 428 ------ .../ContentScanning/TypeProvider.cs | 83 ++ .../UnsupportedMetadataException.cs | 19 + .../PipeHubBuildCommunication.cs | 127 ++ .../Infrastructure/TypeExtensions.cs | 87 ++ .../Models/ConfigFile/HubClientConfig.cs | 40 +- .../Models/ConfigFile/Preferences.cs | 43 +- .../ConfigFile/SandboxConfig.cs} | 9 +- .../{ => Models}/ContentScanning/MType.cs | 2 +- .../ScanningTypes/MMemberRef.cs | 13 + .../ScanningTypes/MMemberRefField.cs | 16 + .../ScanningTypes/MMemberRefMethod.cs | 23 + .../ScanningTypes/MResScope.cs | 5 + .../ScanningTypes/MResScopeAssembly.cs | 9 + .../ScanningTypes/MResScopeType.cs | 10 + .../ScanningTypes/MTypeByRef.cs | 19 + .../ScanningTypes/MTypeDefined.cs | 21 + .../ScanningTypes/MTypeGeneric.cs | 68 + .../MTypeGenericMethodPlaceHolder.cs | 14 + .../MTypeGenericTypePlaceHolder.cs | 14 + .../ScanningTypes/MTypeModified.cs | 21 + .../ScanningTypes/MTypeParsed.cs | 38 + .../ScanningTypes/MTypePointer.cs | 19 + .../ScanningTypes/MTypePrimitive.cs | 67 + .../ScanningTypes/MTypeReferenced.cs | 37 + .../ScanningTypes/MTypeSZArray.cs | 20 + .../ScanningTypes/MTypeWackyArray.cs | 34 + .../ContentScanning/TypeConfig.cs | 5 +- .../ContentScanning/WhitelistFieldDefine.cs | 2 +- .../ContentScanning/WhitelistMethodDefine.cs | 2 +- .../Models/Enums/ClientRequest.cs | 8 + .../Models/Enums/DumpFlags.cs | 14 + .../Models/Enums/InheritMode.cs | 2 +- .../Services/AssemblyTypeCheckerService.cs | 623 +++++++++ .../Services/CodeScanConfigService.cs | 291 ++++ .../Services/CodeScanService.cs | 61 +- UnitystationLauncher/Services/FileService.cs | 18 - .../Services/GameCommunicationPipeService.cs | 16 + .../Services/GoodFileService.cs | 137 -- .../HubBuildCommunicationPipeService.cs | 147 -- .../Services/InstallationService.cs | 24 +- ...cker.cs => IAssemblyTypeCheckerService.cs} | 2 +- ...leService.cs => ICodeScanConfigService.cs} | 5 +- .../Services/Interface/IFileService.cs | 10 - .../IGameCommunicationPipeService.cs | 4 + UnitystationLauncher/StandardModule.cs | 10 +- .../ViewModels/LauncherViewModel.cs | 8 +- 55 files changed, 2407 insertions(+), 2350 deletions(-) delete mode 100644 UnitystationLauncher/ContentScanning/AssemblyTypeChecker.Config.cs delete mode 100644 UnitystationLauncher/ContentScanning/AssemblyTypeChecker.cs create mode 100644 UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs create mode 100644 UnitystationLauncher/ContentScanning/FileInfoComparer.cs delete mode 100644 UnitystationLauncher/ContentScanning/MetaMembersHelper.cs create mode 100644 UnitystationLauncher/ContentScanning/Resolver.cs create mode 100644 UnitystationLauncher/ContentScanning/SandboxError.cs delete mode 100644 UnitystationLauncher/ContentScanning/ScanningTypes.cs create mode 100644 UnitystationLauncher/ContentScanning/TypeProvider.cs create mode 100644 UnitystationLauncher/Exceptions/UnsupportedMetadataException.cs create mode 100644 UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs create mode 100644 UnitystationLauncher/Infrastructure/TypeExtensions.cs rename UnitystationLauncher/{ContentScanning/ScanConfigMode.cs => Models/ConfigFile/SandboxConfig.cs} (80%) rename UnitystationLauncher/{ => Models}/ContentScanning/MType.cs (87%) create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRef.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefField.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefMethod.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScope.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeAssembly.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeType.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeByRef.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeDefined.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericMethodPlaceHolder.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericTypePlaceHolder.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeModified.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeParsed.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePointer.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePrimitive.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs create mode 100644 UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs rename UnitystationLauncher/{ => Models}/ContentScanning/TypeConfig.cs (87%) rename UnitystationLauncher/{ => Models}/ContentScanning/WhitelistFieldDefine.cs (80%) rename UnitystationLauncher/{ => Models}/ContentScanning/WhitelistMethodDefine.cs (91%) create mode 100644 UnitystationLauncher/Models/Enums/ClientRequest.cs create mode 100644 UnitystationLauncher/Models/Enums/DumpFlags.cs create mode 100644 UnitystationLauncher/Services/AssemblyTypeCheckerService.cs create mode 100644 UnitystationLauncher/Services/CodeScanConfigService.cs delete mode 100644 UnitystationLauncher/Services/FileService.cs create mode 100644 UnitystationLauncher/Services/GameCommunicationPipeService.cs delete mode 100644 UnitystationLauncher/Services/GoodFileService.cs delete mode 100644 UnitystationLauncher/Services/HubBuildCommunicationPipeService.cs rename UnitystationLauncher/Services/Interface/{IAssemblyChecker.cs => IAssemblyTypeCheckerService.cs} (85%) rename UnitystationLauncher/Services/Interface/{IGoodFileService.cs => ICodeScanConfigService.cs} (67%) delete mode 100644 UnitystationLauncher/Services/Interface/IFileService.cs create mode 100644 UnitystationLauncher/Services/Interface/IGameCommunicationPipeService.cs diff --git a/UnitystationLauncher/ContentScanning/AssemblyTypeChecker.Config.cs b/UnitystationLauncher/ContentScanning/AssemblyTypeChecker.Config.cs deleted file mode 100644 index 2cb20880..00000000 --- a/UnitystationLauncher/ContentScanning/AssemblyTypeChecker.Config.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using ILVerify; -using Pidgin; -using Serilog; -using UnitystationLauncher.Services.Interface; - -namespace UnitystationLauncher.ContentScanning; - -public sealed partial class AssemblyTypeChecker -{ - private static string NameConfig = @"CodeScanList.json"; - - private async Task LoadConfig() - { - var configPath = Path.Combine(_environmentService.GetUserdataDirectory(), NameConfig); - try - { - var response = await _httpClient.GetAsync("https://raw.githubusercontent.com/unitystation/unitystation/develop/CodeScanList.json"); - if (response.IsSuccessStatusCode) - { - var jsonData = await response.Content.ReadAsStringAsync(); - File.Delete(configPath); - await File.WriteAllTextAsync(configPath, jsonData); - Console.WriteLine("JSON file saved successfully."); - } - else - { - Log.Error("Unable to download config" + response.ToString()); - } - } - catch (Exception e) - { - Log.Error("Unable to download config" + e.ToString()); - } - - - if (_fileService.Exists(configPath) == false) - { - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = "UnitystationLauncher.CodeScanList.json"; - using (var stream = assembly.GetManifestResourceStream(resourceName)) - { - if (stream != null) - { - // Copy the contents of the resource to a file location - using (var fileStream = File.Create(configPath)) - { - stream.Seek(0L, SeekOrigin.Begin); - await stream.CopyToAsync(fileStream); - } - } - } - Log.Error("had to use backup config"); - } - - using (StreamReader file = _fileService.OpenText(configPath)) - { - try - { - var data = JsonSerializer.Deserialize(file.ReadToEnd(), new JsonSerializerOptions - { - AllowTrailingCommas = true, - Converters = - { - new JsonStringEnumConverter(allowIntegerValues: false) - } - }); - - if (data == null) - { - Log.Error("unable to de-serialise config"); - throw new DataException("unable to de-serialise config"); - } - - foreach (var @namespace in data.Types) - { - foreach (var @class in @namespace.Value) - { - ParseTypeConfig(@class.Value); - } - } - - return data; - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - } - - private static void ParseTypeConfig(TypeConfig cfg) - { - if (cfg.Methods != null) - { - var list = new List(); - foreach (var m in cfg.Methods) - { - try - { - list.Add(Parsers.MethodParser.ParseOrThrow(m)); - } - catch (ParseException e) - { - Log.Error($"Parse exception for '{m}': {e}"); - } - } - - cfg.MethodsParsed = list.ToArray(); - } - else - { - cfg.MethodsParsed = Array.Empty(); - } - - if (cfg.Fields != null) - { - var list = new List(); - foreach (var f in cfg.Fields) - { - try - { - list.Add(Parsers.FieldParser.ParseOrThrow(f)); - } - catch (ParseException e) - { - Log.Error($"Parse exception for '{f}': {e}"); - throw; - } - } - - cfg.FieldsParsed = list.ToArray(); - } - else - { - cfg.FieldsParsed = Array.Empty(); - } - - if (cfg.NestedTypes != null) - { - foreach (var nested in cfg.NestedTypes.Values) - { - ParseTypeConfig(nested); - } - } - } -} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/AssemblyTypeChecker.cs b/UnitystationLauncher/ContentScanning/AssemblyTypeChecker.cs deleted file mode 100644 index 453069c2..00000000 --- a/UnitystationLauncher/ContentScanning/AssemblyTypeChecker.cs +++ /dev/null @@ -1,1243 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; -using System.Threading.Tasks; -using ILVerify; -using MoreLinq; -using UnitystationLauncher.Services.Interface; - -// psst -// You know ECMA-335 right? The specification for the CLI that .NET runs on? -// Yeah, you need it to understand a lot of this code. So get a copy. -// You know the cool thing? -// ISO has a version that has correct PDF metadata so there's an actual table of contents. -// Right here: https://standards.iso.org/ittf/PubliclyAvailableStandards/c058046_ISO_IEC_23271_2012(E).zip - -namespace UnitystationLauncher.ContentScanning; - -/// -/// Manages the type white/black list of types and namespaces, and verifies assemblies against them. -/// -public sealed partial class AssemblyTypeChecker : IAssemblyChecker -{ - // Used to be in Sandbox.yml, moved out of there to facilitate faster loading. - private const string SystemAssemblyName = "mscorlib"; //TODO check security - //UnityEngine.dll - //mscorlib - //System.Runtime - - /// - /// Completely disables type checking, allowing everything. - /// - public bool DisableTypeCheck { get; init; } - - public DumpFlags Dump { get; init; } = DumpFlags.None; - public bool VerifyIl { get; init; } - - private readonly Task _config; - - private readonly IEnvironmentService _environmentService; - - private readonly IFileService _fileService; - - private readonly HttpClient _httpClient; - - public AssemblyTypeChecker(IEnvironmentService environmentService, IFileService FileService, HttpClient httpClient) - { - _environmentService = environmentService; - VerifyIl = true; - DisableTypeCheck = false; - _fileService = FileService; - _httpClient = httpClient; - _config = Task.Run(LoadConfig); - } - - - private sealed class Resolver : IResolver - { - private readonly DirectoryInfo _managedPath; - - - private readonly Dictionary _dictionaryLookup = new Dictionary(); - - public void Dispose() - { - foreach (var Lookup in _dictionaryLookup) - { - Lookup.Value.Dispose(); - } - } - - PEReader IResolver.ResolveAssembly(AssemblyName assemblyName) - { - if (assemblyName.Name == null) - { - throw new FileNotFoundException("Unable to find " + assemblyName.FullName); - } - - if (_dictionaryLookup.TryGetValue(assemblyName.Name, out var assembly)) - { - return assembly; - } - - FileInfo[] files = _managedPath.GetFiles("*.dll"); // Change the file extension to match your DLLs - - foreach (FileInfo file in files) - { - string fileName = Path.GetFileNameWithoutExtension(file.Name); - if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); - _dictionaryLookup[assemblyName.Name] = - new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); - return _dictionaryLookup[assemblyName.Name]; - } - } - - files = _managedPath.GetFiles("*.so"); // Change the file extension to match Linux stuff to - - foreach (FileInfo file in files) - { - string fileName = Path.GetFileNameWithoutExtension(file.Name); - if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); - _dictionaryLookup[assemblyName.Name] = - new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); - return _dictionaryLookup[assemblyName.Name]; - } - } - - files = _managedPath.GetFiles("*.dylib"); // Change the file extension to match mac stuff to - - foreach (FileInfo file in files) - { - string fileName = Path.GetFileNameWithoutExtension(file.Name); - if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); - _dictionaryLookup[assemblyName.Name] = - new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); - return _dictionaryLookup[assemblyName.Name]; - } - } - - - - throw new FileNotFoundException("Unable to find it " + assemblyName.FullName); - } - - public Resolver(DirectoryInfo inManagedPath) - { - _managedPath = inManagedPath; - } - - PEReader IResolver.ResolveModule(AssemblyName referencingAssembly, string fileName) - { - //TODO idk This is never used anywhere - throw new NotImplementedException( - $"idk How IResolver.ResolveModule(AssemblyName {referencingAssembly}, string {fileName}) , And it's never been called so.. "); - } - } - - private Resolver CreateResolver(DirectoryInfo ManagedPath) - { - return new Resolver(ManagedPath); - } - - /// - /// Check the assembly for any illegal types. Any types not on the white list - /// will cause the assembly to be rejected. - /// - /// - /// - /// - /// - /// - /// Assembly to load. - /// - public bool CheckAssembly(FileInfo diskPath, DirectoryInfo managedPath, List otherAssemblies, - Action info, Action Errors) - { - using var assembly = diskPath.OpenRead(); - var fullStopwatch = Stopwatch.StartNew(); - - var resolver = CreateResolver(managedPath); - using var peReader = new PEReader(assembly, PEStreamOptions.LeaveOpen); - var reader = peReader.GetMetadataReader(); - - var asmName = reader.GetString(reader.GetAssemblyDefinition().Name); - - if (peReader.PEHeaders.CorHeader?.ManagedNativeHeaderDirectory is { Size: not 0 }) - { - Errors.Invoke($"Assembly {asmName} contains native code."); - return false; - } - - if (VerifyIl) - { - if (DoVerifyIL(asmName, resolver, peReader, reader, info, Errors) == false) - { - Errors.Invoke($"Assembly {asmName} Has invalid IL code"); - return false; - } - } - - - var errors = new ConcurrentBag(); - - var types = GetReferencedTypes(reader, errors); - var members = GetReferencedMembers(reader, errors); - var inherited = GetExternalInheritedTypes(reader, errors); - info.Invoke($"References loaded... {fullStopwatch.ElapsedMilliseconds}ms"); - - if (DisableTypeCheck) - { - resolver.Dispose(); - peReader.Dispose(); - return true; - } - - - var loadedConfig = _config.Result; - - loadedConfig.MultiAssemblyOtherReferences.Clear(); - loadedConfig.MultiAssemblyOtherReferences.AddRange(otherAssemblies); - - // We still do explicit type reference scanning, even though the actual whitelists work with raw members. - // This is so that we can simplify handling of generic type specifications during member checking: - // we won't have to check that any types in their type arguments are whitelisted. - foreach (var type in types) - { - if (IsTypeAccessAllowed(loadedConfig, type, out _) == false) - { - errors.Add(new SandboxError($"Access to type not allowed: {type} asmName {asmName}")); - } - } - - info.Invoke($"Types... {fullStopwatch.ElapsedMilliseconds}ms"); - - CheckInheritance(loadedConfig, inherited, errors); - - info.Invoke($"Inheritance... {fullStopwatch.ElapsedMilliseconds}ms"); - - CheckNoUnmanagedMethodDefs(reader, errors); - - info.Invoke($"Unmanaged methods... {fullStopwatch.ElapsedMilliseconds}ms"); - - CheckNoTypeAbuse(reader, errors); - - info.Invoke($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms"); - - CheckMemberReferences(loadedConfig, members, errors); - - errors = new ConcurrentBag(errors.OrderBy(x => x.Message)); - - foreach (var error in errors) - { - Errors.Invoke($"Sandbox violation: {error.Message}"); - } - - info.Invoke($"Checked assembly in {fullStopwatch.ElapsedMilliseconds}ms"); - resolver.Dispose(); - peReader.Dispose(); - return errors.IsEmpty; - } - - private bool DoVerifyIL( - string name, - IResolver resolver, - PEReader peReader, - MetadataReader reader, - Action info, - Action logErrors) - { - info.Invoke($"{name}: Verifying IL..."); - var sw = Stopwatch.StartNew(); - var bag = new ConcurrentBag(); - - - var UesParallel = false; - - if (UesParallel) - { - var partitioner = Partitioner.Create(reader.TypeDefinitions); - Parallel.ForEach(partitioner.GetPartitions(Environment.ProcessorCount), handle => - { - var ver = new Verifier(resolver); - ver.SetSystemModuleName(new AssemblyName(SystemAssemblyName)); - while (handle.MoveNext()) - { - foreach (var result in ver.Verify(peReader, handle.Current, verifyMethods: true)) - { - bag.Add(result); - } - } - }); - } - else - { - var ver = new Verifier(resolver); - //mscorlib - ver.SetSystemModuleName(new AssemblyName(SystemAssemblyName)); - foreach (var Definition in reader.TypeDefinitions) - { - var Errors = ver.Verify(peReader, Definition, verifyMethods: true); - foreach (var Error in Errors) - { - bag.Add(Error); - } - } - } - - var loadedCfg = _config.Result; - - var verifyErrors = false; - foreach (var res in bag) - { - if (loadedCfg.AllowedVerifierErrors.Contains(res.Code)) - { - continue; - } - - var formatted = res.Args == null ? res.Message : string.Format(res.Message, res.Args); - var msg = $"{name}: ILVerify: {formatted}"; - - if (!res.Method.IsNil) - { - var method = reader.GetMethodDefinition(res.Method); - var methodName = FormatMethodName(reader, method); - - msg = $"{msg}, method: {methodName}"; - } - - if (!res.Type.IsNil) - { - var type = GetTypeFromDefinition(reader, res.Type); - msg = $"{msg}, type: {type}"; - } - - - verifyErrors = true; - logErrors.Invoke(msg); - } - - info.Invoke($"{name}: Verified IL in {sw.Elapsed.TotalMilliseconds}ms"); - - if (verifyErrors) - { - return false; - } - - return true; - } - - private static string FormatMethodName(MetadataReader reader, MethodDefinition method) - { - var methodSig = method.DecodeSignature(new TypeProvider(), 0); - var type = GetTypeFromDefinition(reader, method.GetDeclaringType()); - - return - $"{type}.{reader.GetString(method.Name)}({string.Join(", ", methodSig.ParameterTypes)}) Returns {methodSig.ReturnType} "; - } - - private static void CheckNoUnmanagedMethodDefs(MetadataReader reader, ConcurrentBag errors) - { - foreach (var methodDefHandle in reader.MethodDefinitions) - { - var methodDef = reader.GetMethodDefinition(methodDefHandle); - var implAttr = methodDef.ImplAttributes; - var attr = methodDef.Attributes; - - if ((implAttr & MethodImplAttributes.Unmanaged) != 0 || - (implAttr & MethodImplAttributes.CodeTypeMask) is not (MethodImplAttributes.IL - or MethodImplAttributes.Runtime)) - { - var err = $"Method has illegal MethodImplAttributes: {FormatMethodName(reader, methodDef)}"; - errors.Add(new SandboxError(err)); - } - - if ((attr & (MethodAttributes.PinvokeImpl | MethodAttributes.UnmanagedExport)) != 0) - { - var err = $"Method has illegal MethodAttributes: {FormatMethodName(reader, methodDef)}"; - errors.Add(new SandboxError(err)); - } - } - } - - private static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag errors) - { - foreach (var typeDefHandle in reader.TypeDefinitions) - { - var typeDef = reader.GetTypeDefinition(typeDefHandle); - if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) != 0) - { - // The C# compiler emits explicit layout types for some array init logic. These have no fields. - // Only ban explicit layout if it has fields. - - var type = GetTypeFromDefinition(reader, typeDefHandle); - - if (typeDef.GetFields().Count > 0) - { - var err = $"Explicit layout type {type} may not have fields."; - errors.Add(new SandboxError(err)); - } - } - } - } - - private void CheckMemberReferences( - SandboxConfig sandboxConfig, - List members, - ConcurrentBag errors) - { - bool IsParallel = true; - - if (IsParallel) - { - Parallel.ForEach(members, memberRef => - { - MType baseType = memberRef.ParentType; - while (!(baseType is ScanningTypes.MTypeReferenced)) - { - switch (baseType) - { - case ScanningTypes.MTypeGeneric generic: - { - baseType = generic.GenericType; - - break; - } - case ScanningTypes.MTypeWackyArray: - { - // Members on arrays (not to be confused with vectors) are all fine. - // See II.14.2 in ECMA-335. - return; - } - case ScanningTypes.MTypeDefined: - { - // Valid for this to show up, safe to ignore. - return; - } - default: - { - throw new ArgumentOutOfRangeException(); - } - } - } - - var baseTypeReferenced = (ScanningTypes.MTypeReferenced)baseType; - - if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out var typeCfg) == false) - { - // Technically this error isn't necessary since we have an earlier pass - // checking all referenced types. That should have caught this - // We still need the typeCfg so that's why we're checking. Might as well. - errors.Add(new SandboxError($"Access to type not allowed: {baseTypeReferenced}")); - return; - } - - if (typeCfg.All) - { - // Fully whitelisted for the type, we good. - return; - } - - switch (memberRef) - { - case ScanningTypes.MMemberRefField mMemberRefField: - { - foreach (var field in typeCfg.FieldsParsed) - { - if (field.Name == mMemberRefField.Name && - mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) - { - return; // Found - } - } - - errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}")); - break; - } - case ScanningTypes.MMemberRefMethod mMemberRefMethod: - foreach (var parsed in typeCfg.MethodsParsed) - { - bool notParamMismatch = true; - - if (parsed.Name == mMemberRefMethod.Name && - mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && - mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && - mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) - { - for (var i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) - { - var a = mMemberRefMethod.ParameterTypes[i]; - var b = parsed.ParameterTypes[i]; - - if (a.WhitelistEquals(b) == false) - { - notParamMismatch = false; - break; - } - } - - if (notParamMismatch) - { - return; // Found - } - } - } - - errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}")); - break; - default: - throw new ArgumentOutOfRangeException(nameof(memberRef)); - } - }); - } - else - { - foreach (var memberRef in members) - { - MType baseType = memberRef.ParentType; - while (!(baseType is ScanningTypes.MTypeReferenced)) - { - switch (baseType) - { - case ScanningTypes.MTypeGeneric generic: - { - baseType = generic.GenericType; - - break; - } - case ScanningTypes.MTypeWackyArray: - { - // Members on arrays (not to be confused with vectors) are all fine. - // See II.14.2 in ECMA-335. - continue; - } - case ScanningTypes.MTypeDefined: - { - // Valid for this to show up, safe to ignore. - continue; - } - default: - { - throw new ArgumentOutOfRangeException(); - } - } - } - - var baseTypeReferenced = (ScanningTypes.MTypeReferenced)baseType; - - if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out var typeCfg) == false) - { - // Technically this error isn't necessary since we have an earlier pass - // checking all referenced types. That should have caught this - // We still need the typeCfg so that's why we're checking. Might as well. - errors.Add(new SandboxError($"Access to type not allowed: {baseTypeReferenced}")); - continue; - } - - if (typeCfg.All) - { - // Fully whitelisted for the type, we good. - continue; - } - - switch (memberRef) - { - case ScanningTypes.MMemberRefField mMemberRefField: - { - foreach (var field in typeCfg.FieldsParsed) - { - if (field.Name == mMemberRefField.Name && - mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) - { - continue; // Found - } - } - - errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}")); - break; - } - case ScanningTypes.MMemberRefMethod mMemberRefMethod: - bool notParamMismatch = true; - foreach (var parsed in typeCfg.MethodsParsed) - { - if (parsed.Name == mMemberRefMethod.Name && - mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && - mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && - mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) - { - for (var i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) - { - var a = mMemberRefMethod.ParameterTypes[i]; - var b = parsed.ParameterTypes[i]; - - if (!a.WhitelistEquals(b)) - { - notParamMismatch = false; - break; - - } - } - - if (notParamMismatch) - { - break; // Found - } - break; - } - } - - if (notParamMismatch == false) - { - continue; - } - - errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}")); - break; - default: - throw new ArgumentOutOfRangeException(nameof(memberRef)); - } - } - } - } - - private void CheckInheritance( - SandboxConfig sandboxConfig, - List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited, - ConcurrentBag errors) - { - // This inheritance whitelisting primarily serves to avoid content doing funny stuff - // by e.g. inheriting Type. - foreach (var (_, baseType, interfaces) in inherited) - { - if (CanInherit(baseType) == false) - { - errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType}")); - } - - foreach (var @interface in interfaces) - { - if (CanInherit(@interface) == false) - { - errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface}")); - } - } - - bool CanInherit(MType inheritType) - { - var realBaseType = inheritType switch - { - ScanningTypes.MTypeGeneric generic => (ScanningTypes.MTypeReferenced)generic.GenericType, - ScanningTypes.MTypeReferenced referenced => referenced, - _ => throw new InvalidOperationException() // Can't happen. - }; - - if (IsTypeAccessAllowed(sandboxConfig, realBaseType, out var cfg) == false) - { - return false; - } - - return cfg.Inherit != InheritMode.Block && (cfg.Inherit == InheritMode.Allow || cfg.All); - } - } - } - - private bool IsTypeAccessAllowed(SandboxConfig sandboxConfig, ScanningTypes.MTypeReferenced type, - [NotNullWhen(true)] out TypeConfig? cfg) - { - if (type.Namespace == null) - { - if (type.ResolutionScope is ScanningTypes.MResScopeType parentType) - { - if (IsTypeAccessAllowed(sandboxConfig, (ScanningTypes.MTypeReferenced)parentType.Type, out var parentCfg) == false) - { - cfg = null; - return false; - } - - if (parentCfg.All) - { - // Enclosing type is namespace-whitelisted so we don't have to check anything else. - cfg = TypeConfig.DefaultAll; - return true; - } - - // Found enclosing type, checking if we are allowed to access this nested type. - // Also pass it up in case of multiple nested types. - if (parentCfg.NestedTypes != null && parentCfg.NestedTypes.TryGetValue(type.Name, out cfg)) - { - return true; - } - - cfg = null; - return false; - } - - if (type.ResolutionScope is ScanningTypes.MResScopeAssembly mResScopeAssembly && - sandboxConfig.MultiAssemblyOtherReferences.Contains(mResScopeAssembly.Name)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - - // Types without namespaces or nesting parent are not allowed at all. - cfg = null; - return false; - } - - // Check if in whitelisted namespaces. - foreach (var whNamespace in sandboxConfig.WhitelistedNamespaces) - { - if (type.Namespace.StartsWith(whNamespace)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - } - - if (type.ResolutionScope is ScanningTypes.MResScopeAssembly resScopeAssembly && - sandboxConfig.MultiAssemblyOtherReferences.Contains(resScopeAssembly.Name)) - { - cfg = TypeConfig.DefaultAll; - return true; - } - - - if (sandboxConfig.Types.TryGetValue(type.Namespace, out var nsDict) == false) - { - cfg = null; - return false; - } - - return nsDict.TryGetValue(type.Name, out cfg); - } - - private List GetReferencedTypes(MetadataReader reader, ConcurrentBag errors) - { - return reader.TypeReferences.Select(typeRefHandle => - { - try - { - return ParseTypeReference(reader, typeRefHandle); - } - catch (UnsupportedMetadataException e) - { - errors.Add(new SandboxError(e)); - return null; - } - }) - .Where(p => p != null) - .ToList()!; - } - - private List GetReferencedMembers(MetadataReader reader, ConcurrentBag errors) - { - bool Parallel = true; - if (Parallel) - { - return reader.MemberReferences.AsParallel() - .Select(memRefHandle => - { - var memRef = reader.GetMemberReference(memRefHandle); - var memName = reader.GetString(memRef.Name); - MType parent; - switch (memRef.Parent.Kind) - { - // See II.22.25 in ECMA-335. - case HandleKind.TypeReference: - { - // Regular type reference. - try - { - parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeDefinition: - { - try - { - parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeSpecification: - { - var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); - // Generic type reference. - var provider = new TypeProvider(); - parent = typeSpec.DecodeSignature(provider, 0); - - if (parent.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics since MethodSpec needs to point to MemberRef. - return null; - } - - break; - } - case HandleKind.ModuleReference: - { - errors.Add(new SandboxError( - $"Module global variables and methods are unsupported. Name: {memName}")); - return null; - } - case HandleKind.MethodDefinition: - { - errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}")); - return null; - } - default: - { - errors.Add(new SandboxError( - $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); - return null; - } - } - - ScanningTypes.MMemberRef memberRef; - - switch (memRef.GetKind()) - { - case MemberReferenceKind.Method: - { - var sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); - - memberRef = new ScanningTypes.MMemberRefMethod( - parent, - memName, - sig.ReturnType, - sig.GenericParameterCount, - sig.ParameterTypes); - - break; - } - case MemberReferenceKind.Field: - { - var fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); - memberRef = new ScanningTypes.MMemberRefField(parent, memName, fieldType); - break; - } - default: - throw new ArgumentOutOfRangeException(); - } - - return memberRef; - }) - .Where(p => p != null) - .ToList()!; - } - else - { - return reader.MemberReferences.Select(memRefHandle => - { - var memRef = reader.GetMemberReference(memRefHandle); - var memName = reader.GetString(memRef.Name); - MType parent; - switch (memRef.Parent.Kind) - { - // See II.22.25 in ECMA-335. - case HandleKind.TypeReference: - { - // Regular type reference. - try - { - parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeDefinition: - { - try - { - parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return null; - } - - break; - } - case HandleKind.TypeSpecification: - { - var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); - // Generic type reference. - var provider = new TypeProvider(); - parent = typeSpec.DecodeSignature(provider, 0); - - if (parent.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics since MethodSpec needs to point to MemberRef. - return null; - } - - break; - } - case HandleKind.ModuleReference: - { - errors.Add(new SandboxError( - $"Module global variables and methods are unsupported. Name: {memName}")); - return null; - } - case HandleKind.MethodDefinition: - { - errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}")); - return null; - } - default: - { - errors.Add(new SandboxError( - $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); - return null; - } - } - - ScanningTypes.MMemberRef memberRef; - - switch (memRef.GetKind()) - { - case MemberReferenceKind.Method: - { - var sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); - - memberRef = new ScanningTypes.MMemberRefMethod( - parent, - memName, - sig.ReturnType, - sig.GenericParameterCount, - sig.ParameterTypes); - - break; - } - case MemberReferenceKind.Field: - { - var fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); - memberRef = new ScanningTypes.MMemberRefField(parent, memName, fieldType); - break; - } - default: - throw new ArgumentOutOfRangeException(); - } - - return memberRef; - }) - .Where(p => p != null) - .ToList()!; - } - } - - private List<(MType type, MType parent, ArraySegment interfaceImpls)> GetExternalInheritedTypes( - MetadataReader reader, - ConcurrentBag errors) - { - var list = new List<(MType, MType, ArraySegment)>(); - foreach (var typeDefHandle in reader.TypeDefinitions) - { - var typeDef = reader.GetTypeDefinition(typeDefHandle); - ArraySegment interfaceImpls; - ScanningTypes.MTypeDefined type = GetTypeFromDefinition(reader, typeDefHandle); - - if (!ParseInheritType(type, typeDef.BaseType, out var parent, reader, errors)) - { - continue; - } - - var interfaceImplsCollection = typeDef.GetInterfaceImplementations(); - if (interfaceImplsCollection.Count == 0) - { - interfaceImpls = Array.Empty(); - } - else - { - interfaceImpls = new MType[interfaceImplsCollection.Count]; - var i = 0; - foreach (var implHandle in interfaceImplsCollection) - { - var interfaceImpl = reader.GetInterfaceImplementation(implHandle); - - if (ParseInheritType(type, interfaceImpl.Interface, out var implemented, reader, errors)) - { - interfaceImpls[i++] = implemented; - } - } - - interfaceImpls = interfaceImpls[..i]; - } - - list.Add((type, parent, interfaceImpls)); - } - - return list; - } - - private bool ParseInheritType(MType ownerType, EntityHandle handle, [NotNullWhen(true)] out MType? type, MetadataReader reader, ConcurrentBag errors) - { - type = default; - - switch (handle.Kind) - { - case HandleKind.TypeDefinition: - // Definition to type in same assembly, allowed without hassle. - return false; - - case HandleKind.TypeReference: - // Regular type reference. - try - { - type = ParseTypeReference(reader, (TypeReferenceHandle)handle); - return true; - } - catch (UnsupportedMetadataException u) - { - errors.Add(new SandboxError(u)); - return false; - } - - case HandleKind.TypeSpecification: - var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle); - // Generic type reference. - var provider = new TypeProvider(); - type = typeSpec.DecodeSignature(provider, 0); - - if (type.IsCoreTypeDefined()) - { - // Ensure this isn't a self-defined type. - // This can happen due to generics. - return false; - } - - break; - - default: - errors.Add(new SandboxError( - $"Unsupported BaseType of kind {handle.Kind} on type {ownerType}")); - return false; - } - - type = default!; - return false; - } - - - - private sealed class SandboxError - { - public string Message; - - public SandboxError(string message) - { - Message = message; - } - - public SandboxError(UnsupportedMetadataException ume) : this($"Unsupported metadata: {ume.Message}") - { - } - } - - - /// - /// Thrown if the metadata does something funny we don't "support" like type forwarding. - /// - internal static ScanningTypes.MTypeReferenced ParseTypeReference(MetadataReader reader, TypeReferenceHandle handle) - { - var typeRef = reader.GetTypeReference(handle); - var name = reader.GetString(typeRef.Name); - var nameSpace = NilNullString(reader, typeRef.Namespace); - ScanningTypes.MResScope resScope; - - // See II.22.38 in ECMA-335 - if (typeRef.ResolutionScope.IsNil) - { - throw new UnsupportedMetadataException( - $"Null resolution scope on type Name: {nameSpace}.{name}. This indicates exported/forwarded types"); - } - - switch (typeRef.ResolutionScope.Kind) - { - case HandleKind.AssemblyReference: - { - // Different assembly. - var assemblyRef = - reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); - var assemblyName = reader.GetString(assemblyRef.Name); - resScope = new ScanningTypes.MResScopeAssembly(assemblyName); - break; - } - case HandleKind.TypeReference: - { - // Nested type. - var enclosingType = ParseTypeReference(reader, (TypeReferenceHandle)typeRef.ResolutionScope); - resScope = new ScanningTypes.MResScopeType(enclosingType); - break; - } - case HandleKind.ModuleReference: - { - // Same-assembly-different-module - throw new UnsupportedMetadataException( - $"Cross-module reference to type {nameSpace}.{name}. "); - } - default: - // Edge cases not handled: - // https://github.com/dotnet/runtime/blob/b2e5a89085fcd87e2fa9300b4bb00cd499c5845b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/DisassemblingTypeProvider.cs#L130-L132 - throw new UnsupportedMetadataException( - $"TypeRef to {typeRef.ResolutionScope.Kind} for type {nameSpace}.{name}"); - } - - return new ScanningTypes.MTypeReferenced(resScope, name, nameSpace); - } - - public sealed class UnsupportedMetadataException : Exception - { - public UnsupportedMetadataException() - { - } - - public UnsupportedMetadataException(string message) : base(message) - { - } - - public UnsupportedMetadataException(string message, Exception inner) : base(message, inner) - { - } - } - - public static string? NilNullString(MetadataReader reader, StringHandle handle) - { - return handle.IsNil ? null : reader.GetString(handle); - } - - internal sealed class TypeProvider : ISignatureTypeProvider - { - public MType GetSZArrayType(MType elementType) - { - return new ScanningTypes.MTypeSZArray(elementType); - } - - public MType GetArrayType(MType elementType, ArrayShape shape) - { - return new ScanningTypes.MTypeWackyArray(elementType, shape); - } - - public MType GetByReferenceType(MType elementType) - { - return new ScanningTypes.MTypeByRef(elementType); - } - - public MType GetGenericInstantiation(MType genericType, ImmutableArray typeArguments) - { - return new ScanningTypes.MTypeGeneric(genericType, typeArguments); - } - - public MType GetPointerType(MType elementType) - { - return new ScanningTypes.MTypePointer(elementType); - } - - public MType GetPrimitiveType(PrimitiveTypeCode typeCode) - { - return new ScanningTypes.MTypePrimitive(typeCode); - } - - public MType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - { - return AssemblyTypeChecker.GetTypeFromDefinition(reader, handle); - } - - public MType GetTypeFromReference(MetadataReader inReader, TypeReferenceHandle inHandle, byte inRawTypeKind) - { - return ParseTypeReference(inReader, inHandle); - } - - public MType GetFunctionPointerType(MethodSignature signature) - { - throw new NotImplementedException(); - } - - public MType GetGenericMethodParameter(int genericContext, int index) - { - return new ScanningTypes.MTypeGenericMethodPlaceHolder(index); - } - - public MType GetGenericTypeParameter(int genericContext, int index) - { - return new ScanningTypes.MTypeGenericTypePlaceHolder(index); - } - - public MType GetModifiedType(MType modifier, MType unmodifiedType, bool isRequired) - { - return new ScanningTypes.MTypeModified(unmodifiedType, modifier, isRequired); - } - - public MType GetPinnedType(MType elementType) - { - throw new NotImplementedException(); - } - - public MType GetTypeFromSpecification(MetadataReader reader, int genericContext, - TypeSpecificationHandle handle, - byte rawTypeKind) - { - return reader.GetTypeSpecification(handle).DecodeSignature(this, 0); - } - } - - private static ScanningTypes.MTypeDefined GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle) - { - var typeDef = reader.GetTypeDefinition(handle); - var name = reader.GetString(typeDef.Name); - var ns = NilNullString(reader, typeDef.Namespace); - ScanningTypes.MTypeDefined? enclosing = null; - if (typeDef.IsNested) - { - enclosing = GetTypeFromDefinition(reader, typeDef.GetDeclaringType()); - } - - return new ScanningTypes.MTypeDefined(name, ns, enclosing); - } - - [Flags] - public enum DumpFlags : byte - { - None = 0, - Types = 1, - Members = 2, - Inheritance = 4, - - All = Types | Members | Inheritance - } -} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs b/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs new file mode 100644 index 00000000..c7689f77 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/AssemblyTypeCheckerHelpers.cs @@ -0,0 +1,447 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using UnitystationLauncher.Exceptions; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +// psst +// You know ECMA-335 right? The specification for the CLI that .NET runs on? +// Yeah, you need it to understand a lot of this code. So get a copy. +// You know the cool thing? +// ISO has a version that has correct PDF metadata so there's an actual table of contents. +// Right here: https://standards.iso.org/ittf/PubliclyAvailableStandards/c058046_ISO_IEC_23271_2012(E).zip + +namespace UnitystationLauncher.ContentScanning; + +/// +/// Manages the type white/black list of types and namespaces, and verifies assemblies against them. +/// +internal static class AssemblyTypeCheckerHelpers +{ + // Used to be in Sandbox.yml, moved out of there to facilitate faster loading. + internal const string SystemAssemblyName = "mscorlib"; //TODO check security + //UnityEngine.dll + //mscorlib + //System.Runtime + + internal static Resolver CreateResolver(DirectoryInfo ManagedPath) + { + return new Resolver(ManagedPath); + } + + internal static string FormatMethodName(MetadataReader reader, MethodDefinition method) + { + MethodSignature methodSig = method.DecodeSignature(new TypeProvider(), 0); + MTypeDefined type = GetTypeFromDefinition(reader, method.GetDeclaringType()); + + return + $"{type}.{reader.GetString(method.Name)}({string.Join(", ", methodSig.ParameterTypes)}) Returns {methodSig.ReturnType} "; + } + + internal static void CheckNoUnmanagedMethodDefs(MetadataReader reader, ConcurrentBag errors) + { + foreach (MethodDefinitionHandle methodDefHandle in reader.MethodDefinitions) + { + MethodDefinition methodDef = reader.GetMethodDefinition(methodDefHandle); + MethodImplAttributes implAttr = methodDef.ImplAttributes; + MethodAttributes attr = methodDef.Attributes; + + if ((implAttr & MethodImplAttributes.Unmanaged) != 0 || + (implAttr & MethodImplAttributes.CodeTypeMask) is not (MethodImplAttributes.IL + or MethodImplAttributes.Runtime)) + { + string err = $"Method has illegal MethodImplAttributes: {FormatMethodName(reader, methodDef)}"; + errors.Add(new SandboxError(err)); + } + + if ((attr & (MethodAttributes.PinvokeImpl | MethodAttributes.UnmanagedExport)) != 0) + { + string err = $"Method has illegal MethodAttributes: {FormatMethodName(reader, methodDef)}"; + errors.Add(new SandboxError(err)); + } + } + } + + internal static void CheckNoTypeAbuse(MetadataReader reader, ConcurrentBag errors) + { + foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); + if ((typeDef.Attributes & TypeAttributes.ExplicitLayout) != 0) + { + // The C# compiler emits explicit layout types for some array init logic. These have no fields. + // Only ban explicit layout if it has fields. + + MTypeDefined type = GetTypeFromDefinition(reader, typeDefHandle); + + if (typeDef.GetFields().Count > 0) + { + string err = $"Explicit layout type {type} may not have fields."; + errors.Add(new SandboxError(err)); + } + } + } + } + + internal static List GetReferencedTypes(MetadataReader reader, ConcurrentBag errors) + { + return reader.TypeReferences.Select(typeRefHandle => + { + try + { + return ParseTypeReference(reader, typeRefHandle); + } + catch (UnsupportedMetadataException e) + { + errors.Add(new SandboxError(e)); + return null; + } + }) + .Where(p => p != null) + .ToList()!; + } + + internal static List GetReferencedMembers(MetadataReader reader, ConcurrentBag errors) + { + bool Parallel = true; + if (Parallel) + { + return reader.MemberReferences.AsParallel() + .Select(memRefHandle => + { + MemberReference memRef = reader.GetMemberReference(memRefHandle); + string memName = reader.GetString(memRef.Name); + MType parent; + switch (memRef.Parent.Kind) + { + // See II.22.25 in ECMA-335. + case HandleKind.TypeReference: + { + // Regular type reference. + try + { + parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new SandboxError(u)); + return null; + } + + break; + } + case HandleKind.TypeDefinition: + { + try + { + parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new SandboxError(u)); + return null; + } + + break; + } + case HandleKind.TypeSpecification: + { + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); + // Generic type reference. + TypeProvider provider = new TypeProvider(); + parent = typeSpec.DecodeSignature(provider, 0); + + if (parent.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics since MethodSpec needs to point to MemberRef. + return null; + } + + break; + } + case HandleKind.ModuleReference: + { + errors.Add(new SandboxError( + $"Module global variables and methods are unsupported. Name: {memName}")); + return null; + } + case HandleKind.MethodDefinition: + { + errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}")); + return null; + } + default: + { + errors.Add(new SandboxError( + $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); + return null; + } + } + + MMemberRef memberRef; + + switch (memRef.GetKind()) + { + case MemberReferenceKind.Method: + { + MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); + + memberRef = new MMemberRefMethod( + parent, + memName, + sig.ReturnType, + sig.GenericParameterCount, + sig.ParameterTypes); + + break; + } + case MemberReferenceKind.Field: + { + MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); + memberRef = new MMemberRefField(parent, memName, fieldType); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + return memberRef; + }) + .Where(p => p != null) + .ToList()!; + } + else + { + return reader.MemberReferences.Select(memRefHandle => + { + MemberReference memRef = reader.GetMemberReference(memRefHandle); + string memName = reader.GetString(memRef.Name); + MType parent; + switch (memRef.Parent.Kind) + { + // See II.22.25 in ECMA-335. + case HandleKind.TypeReference: + { + // Regular type reference. + try + { + parent = ParseTypeReference(reader, (TypeReferenceHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new SandboxError(u)); + return null; + } + + break; + } + case HandleKind.TypeDefinition: + { + try + { + parent = GetTypeFromDefinition(reader, (TypeDefinitionHandle)memRef.Parent); + } + catch (UnsupportedMetadataException u) + { + errors.Add(new SandboxError(u)); + return null; + } + + break; + } + case HandleKind.TypeSpecification: + { + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)memRef.Parent); + // Generic type reference. + TypeProvider provider = new TypeProvider(); + parent = typeSpec.DecodeSignature(provider, 0); + + if (parent.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics since MethodSpec needs to point to MemberRef. + return null; + } + + break; + } + case HandleKind.ModuleReference: + { + errors.Add(new SandboxError( + $"Module global variables and methods are unsupported. Name: {memName}")); + return null; + } + case HandleKind.MethodDefinition: + { + errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}")); + return null; + } + default: + { + errors.Add(new SandboxError( + $"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}")); + return null; + } + } + + MMemberRef memberRef; + + switch (memRef.GetKind()) + { + case MemberReferenceKind.Method: + { + MethodSignature sig = memRef.DecodeMethodSignature(new TypeProvider(), 0); + + memberRef = new MMemberRefMethod( + parent, + memName, + sig.ReturnType, + sig.GenericParameterCount, + sig.ParameterTypes); + + break; + } + case MemberReferenceKind.Field: + { + MType fieldType = memRef.DecodeFieldSignature(new TypeProvider(), 0); + memberRef = new MMemberRefField(parent, memName, fieldType); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + return memberRef; + }) + .Where(p => p != null) + .ToList()!; + } + } + + internal static bool ParseInheritType(MType ownerType, EntityHandle handle, [NotNullWhen(true)] out MType? type, MetadataReader reader, ConcurrentBag errors) + { + type = default; + + switch (handle.Kind) + { + case HandleKind.TypeDefinition: + // Definition to type in same assembly, allowed without hassle. + return false; + + case HandleKind.TypeReference: + // Regular type reference. + try + { + type = ParseTypeReference(reader, (TypeReferenceHandle)handle); + return true; + } + catch (UnsupportedMetadataException u) + { + errors.Add(new SandboxError(u)); + return false; + } + + case HandleKind.TypeSpecification: + TypeSpecification typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle); + // Generic type reference. + TypeProvider provider = new TypeProvider(); + type = typeSpec.DecodeSignature(provider, 0); + + if (type.IsCoreTypeDefined()) + { + // Ensure this isn't a self-defined type. + // This can happen due to generics. + return false; + } + + break; + + default: + errors.Add(new SandboxError( + $"Unsupported BaseType of kind {handle.Kind} on type {ownerType}")); + return false; + } + + type = default!; + return false; + } + + /// + /// Thrown if the metadata does something funny we don't "support" like type forwarding. + /// + internal static MTypeReferenced ParseTypeReference(MetadataReader reader, TypeReferenceHandle handle) + { + TypeReference typeRef = reader.GetTypeReference(handle); + string name = reader.GetString(typeRef.Name); + string? nameSpace = NilNullString(reader, typeRef.Namespace); + MResScope resScope; + + // See II.22.38 in ECMA-335 + if (typeRef.ResolutionScope.IsNil) + { + throw new UnsupportedMetadataException( + $"Null resolution scope on type Name: {nameSpace}.{name}. This indicates exported/forwarded types"); + } + + switch (typeRef.ResolutionScope.Kind) + { + case HandleKind.AssemblyReference: + { + // Different assembly. + AssemblyReference assemblyRef = + reader.GetAssemblyReference((AssemblyReferenceHandle)typeRef.ResolutionScope); + string assemblyName = reader.GetString(assemblyRef.Name); + resScope = new MResScopeAssembly(assemblyName); + break; + } + case HandleKind.TypeReference: + { + // Nested type. + MTypeReferenced enclosingType = ParseTypeReference(reader, (TypeReferenceHandle)typeRef.ResolutionScope); + resScope = new MResScopeType(enclosingType); + break; + } + case HandleKind.ModuleReference: + { + // Same-assembly-different-module + throw new UnsupportedMetadataException( + $"Cross-module reference to type {nameSpace}.{name}. "); + } + default: + // Edge cases not handled: + // https://github.com/dotnet/runtime/blob/b2e5a89085fcd87e2fa9300b4bb00cd499c5845b/src/libraries/System.Reflection.Metadata/tests/Metadata/Decoding/DisassemblingTypeProvider.cs#L130-L132 + throw new UnsupportedMetadataException( + $"TypeRef to {typeRef.ResolutionScope.Kind} for type {nameSpace}.{name}"); + } + + return new MTypeReferenced(resScope, name, nameSpace); + } + + internal static string? NilNullString(MetadataReader reader, StringHandle handle) + { + return handle.IsNil ? null : reader.GetString(handle); + } + + + + internal static MTypeDefined GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle) + { + TypeDefinition typeDef = reader.GetTypeDefinition(handle); + string name = reader.GetString(typeDef.Name); + string? ns = NilNullString(reader, typeDef.Namespace); + MTypeDefined? enclosing = null; + if (typeDef.IsNested) + { + enclosing = GetTypeFromDefinition(reader, typeDef.GetDeclaringType()); + } + + return new MTypeDefined(name, ns, enclosing); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/FileInfoComparer.cs b/UnitystationLauncher/ContentScanning/FileInfoComparer.cs new file mode 100644 index 00000000..0b704464 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/FileInfoComparer.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace UnitystationLauncher.ContentScanning; + +internal class FileInfoComparer : IEqualityComparer +{ + public bool Equals(FileInfo? x, FileInfo? y) + { + if (x == null || y == null) return false; + return x.Name.Equals(y.Name, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(FileInfo obj) + { + return obj.Name.GetHashCode(); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/MetaMembersHelper.cs b/UnitystationLauncher/ContentScanning/MetaMembersHelper.cs deleted file mode 100644 index 3d4d53a8..00000000 --- a/UnitystationLauncher/ContentScanning/MetaMembersHelper.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Reflection.Metadata; -using System.Reflection.PortableExecutable; -using Internal.TypeSystem.Ecma; - -namespace UnitystationLauncher.ContentScanning; - -public static class MetaMembersHelper -{ - public static IEnumerable DumpMetaMembers(Type type) - { - var assemblyLoc = type.Assembly.Location; - - // Load assembly with System.Reflection.Metadata. - using var fs = File.OpenRead(assemblyLoc); - using var peReader = new PEReader(fs); - - var metaReader = peReader.GetMetadataReader(); - - // Find type definition in raw assembly metadata. - // Is there a better way to do this than iterating?? - TypeDefinition typeDef = default; - var found = false; - foreach (var typeDefHandle in metaReader.TypeDefinitions) - { - var tempTypeDef = metaReader.GetTypeDefinition(typeDefHandle); - var name = metaReader.GetString(tempTypeDef.Name); - var @namespace = AssemblyTypeChecker.NilNullString(metaReader, tempTypeDef.Namespace); - if (name == type.Name && @namespace == type.Namespace) - { - typeDef = tempTypeDef; - found = true; - break; - } - } - - if (!found) - { - throw new InvalidOperationException("Type didn't exist??"); - } - - // Dump the list. - var provider = new AssemblyTypeChecker.TypeProvider(); - - foreach (var fieldHandle in typeDef.GetFields()) - { - var fieldDef = metaReader.GetFieldDefinition(fieldHandle); - - if ((fieldDef.Attributes & FieldAttributes.FieldAccessMask) != FieldAttributes.Public) - { - continue; - } - - var fieldName = metaReader.GetString(fieldDef.Name); - var fieldType = fieldDef.DecodeSignature(provider, 0); - - yield return $"{fieldType.WhitelistToString()} {fieldName}"; - } - - foreach (var methodHandle in typeDef.GetMethods()) - { - var methodDef = metaReader.GetMethodDefinition(methodHandle); - - if (!methodDef.Attributes.IsPublic()) - { - continue; - } - - var methodName = metaReader.GetString(methodDef.Name); - var methodSig = methodDef.DecodeSignature(provider, 0); - - var paramString = string.Join(", ", methodSig.ParameterTypes.Select(t => t.WhitelistToString())); - var genericCount = methodSig.GenericParameterCount; - var typeParamString = genericCount == 0 - ? "" - : $"<{new string(',', genericCount - 1)}>"; - - yield return $"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})"; - } - } -} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/Parsers.cs b/UnitystationLauncher/ContentScanning/Parsers.cs index 4bac0d81..d68f7cf3 100644 --- a/UnitystationLauncher/ContentScanning/Parsers.cs +++ b/UnitystationLauncher/ContentScanning/Parsers.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Reflection.Metadata; using Pidgin; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; using static Pidgin.Parser; using static Pidgin.Parser; @@ -87,7 +89,7 @@ public static class Parsers Try(StringTypeParser), Try(ObjectTypeParser), TypedReferenceTypeParser) - .Select(code => (MType)new ScanningTypes.MTypePrimitive(code)).Labelled("Primitive type"); + .Select(code => (MType)new MTypePrimitive(code)).Labelled("Primitive type"); private static readonly Parser NamespacedIdentifier = Token(c => char.IsLetterOrDigit(c) || c == '.' || c == '_' || c == '`') @@ -103,19 +105,19 @@ public static class Parsers private static readonly Parser GenericMethodPlaceholderParser = String("!!") .Then(Digit.AtLeastOnceString()) - .Select(p => (MType)new ScanningTypes.MTypeGenericMethodPlaceHolder(int.Parse(p, CultureInfo.InvariantCulture))); + .Select(p => (MType)new MTypeGenericMethodPlaceHolder(int.Parse(p, CultureInfo.InvariantCulture))); private static readonly Parser GenericTypePlaceholderParser = String("!") .Then(Digit.AtLeastOnceString()) - .Select(p => (MType)new ScanningTypes.MTypeGenericTypePlaceHolder(int.Parse(p, CultureInfo.InvariantCulture))); + .Select(p => (MType)new MTypeGenericTypePlaceHolder(int.Parse(p, CultureInfo.InvariantCulture))); private static readonly Parser GenericPlaceholderParser = Try(GenericTypePlaceholderParser) .Or(Try(GenericMethodPlaceholderParser)).Labelled("Generic placeholder"); - private static readonly Parser TypeNameParser = + private static readonly Parser TypeNameParser = Parser.Map( - (a, b) => b.Aggregate(new ScanningTypes.MTypeParsed(a), (parsed, s) => new ScanningTypes.MTypeParsed(s, parsed)), + (a, b) => b.Aggregate(new MTypeParsed(a), (parsed, s) => new MTypeParsed(s, parsed)), NamespacedIdentifier, Char('/').Then(NamespacedIdentifier).Many()); @@ -125,7 +127,7 @@ public static class Parsers MType type = arg1; if (arg2.HasValue) { - type = new ScanningTypes.MTypeGeneric(type, arg2.Value.ToImmutableArray()); + type = new MTypeGeneric(type, arg2.Value.ToImmutableArray()); } return type; @@ -134,7 +136,7 @@ public static class Parsers GenericParametersParser.Optional()); private static readonly Parser MaybeArrayTypeParser = Parser.Map( - (a, b) => b.Aggregate(a, (type, _) => new ScanningTypes.MTypeSZArray(type)), + (a, b) => b.Aggregate(a, (type, _) => new MTypeSZArray(type)), Try(GenericPlaceholderParser).Or(Try(PrimitiveTypeParser)).Or(ConstructedObjectTypeParser), String("[]").Many()); @@ -142,7 +144,7 @@ public static class Parsers String("ref") .Then(SkipWhitespaces) .Then(MaybeArrayTypeParser) - .Select(t => (MType)new ScanningTypes.MTypeByRef(t)) + .Select(t => (MType)new MTypeByRef(t)) .Labelled("ByRef type"); private static readonly Parser TypeParser = Try(ByRefTypeParser).Or(MaybeArrayTypeParser); diff --git a/UnitystationLauncher/ContentScanning/Resolver.cs b/UnitystationLauncher/ContentScanning/Resolver.cs new file mode 100644 index 00000000..b41dfe4f --- /dev/null +++ b/UnitystationLauncher/ContentScanning/Resolver.cs @@ -0,0 +1,94 @@ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Reflection.PortableExecutable; +using ILVerify; + +namespace UnitystationLauncher.ContentScanning; + +public sealed class Resolver : IResolver +{ + private readonly DirectoryInfo _managedPath; + + + private readonly Dictionary _dictionaryLookup = new Dictionary(); + + public Resolver(DirectoryInfo inManagedPath) + { + _managedPath = inManagedPath; + } + + public void Dispose() + { + foreach (KeyValuePair Lookup in _dictionaryLookup) + { + Lookup.Value.Dispose(); + } + } + + PEReader IResolver.ResolveAssembly(AssemblyName assemblyName) + { + if (assemblyName.Name == null) + { + throw new FileNotFoundException("Unable to find " + assemblyName.FullName); + } + + if (_dictionaryLookup.TryGetValue(assemblyName.Name, out PEReader? assembly)) + { + return assembly; + } + + FileInfo[] files = _managedPath.GetFiles("*.dll"); // Change the file extension to match your DLLs + + foreach (FileInfo file in files) + { + string fileName = Path.GetFileNameWithoutExtension(file.Name); + if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); + _dictionaryLookup[assemblyName.Name] = + new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); + return _dictionaryLookup[assemblyName.Name]; + } + } + + files = _managedPath.GetFiles("*.so"); // Change the file extension to match Linux stuff to + + foreach (FileInfo file in files) + { + string fileName = Path.GetFileNameWithoutExtension(file.Name); + if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); + _dictionaryLookup[assemblyName.Name] = + new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); + return _dictionaryLookup[assemblyName.Name]; + } + } + + files = _managedPath.GetFiles("*.dylib"); // Change the file extension to match mac stuff to + + foreach (FileInfo file in files) + { + string fileName = Path.GetFileNameWithoutExtension(file.Name); + if (string.Equals(fileName, assemblyName.Name, StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine($"Found DLL for assembly '{assemblyName.Name}': {file.FullName}"); + _dictionaryLookup[assemblyName.Name] = + new PEReader(file.Open(FileMode.Open, FileAccess.Read, FileShare.Read)); + return _dictionaryLookup[assemblyName.Name]; + } + } + + throw new FileNotFoundException("Unable to find it " + assemblyName.FullName); + } + + PEReader IResolver.ResolveModule(AssemblyName referencingAssembly, string fileName) + { + //TODO idk This is never used anywhere + throw new NotImplementedException( + $"idk How IResolver.ResolveModule(AssemblyName {referencingAssembly}, string {fileName}) , And it's never been called so.. "); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/SandboxError.cs b/UnitystationLauncher/ContentScanning/SandboxError.cs new file mode 100644 index 00000000..b1fc12c6 --- /dev/null +++ b/UnitystationLauncher/ContentScanning/SandboxError.cs @@ -0,0 +1,17 @@ +using UnitystationLauncher.Exceptions; + +namespace UnitystationLauncher.ContentScanning; + +internal sealed class SandboxError +{ + public string Message; + + public SandboxError(string message) + { + Message = message; + } + + public SandboxError(UnsupportedMetadataException ume) : this($"Unsupported metadata: {ume.Message}") + { + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/ScanningTypes.cs b/UnitystationLauncher/ContentScanning/ScanningTypes.cs deleted file mode 100644 index 0f3e8b9f..00000000 --- a/UnitystationLauncher/ContentScanning/ScanningTypes.cs +++ /dev/null @@ -1,428 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Reflection.Metadata; - -// ReSharper disable MemberCanBePrivate.Global - -namespace UnitystationLauncher.ContentScanning; - -public static class ScanningTypes -{ - public class MMemberRef - { - public readonly MType ParentType; - public readonly string Name; - - protected MMemberRef(MType parentType, string name) - { - ParentType = parentType; - Name = name; - } - } - - internal sealed class MMemberRefMethod : MMemberRef - { - public readonly MType ReturnType; - public readonly int GenericParameterCount; - public readonly ImmutableArray ParameterTypes; - - public MMemberRefMethod(MType parentType, string name, MType returnType, - int genericParameterCount, ImmutableArray parameterTypes) : base(parentType, name) - { - ReturnType = returnType; - GenericParameterCount = genericParameterCount; - ParameterTypes = parameterTypes; - } - - public override string ToString() - { - return $"{ParentType}.{Name}({string.Join(", ", ParameterTypes)}) Returns {ReturnType}"; - } - } - - internal sealed class MMemberRefField : MMemberRef - { - public readonly MType FieldType; - - public MMemberRefField(MType parentType, string name, MType fieldType) : base(parentType, name) - { - FieldType = fieldType; - } - - public override string ToString() - { - return $"{ParentType}.{Name} Returns {FieldType}"; - } - } - - internal sealed record MTypeParsed(string FullName, MTypeParsed? NestedParent = null) : MType - { - public override string ToString() - { - return NestedParent != null ? $"{NestedParent}/{FullName}" : FullName; - } - - public override bool WhitelistEquals(MType other) - { - switch (other) - { - case MTypeParsed parsed: - if (NestedParent != null - && (parsed.NestedParent == null || NestedParent.WhitelistEquals(parsed.NestedParent) == false)) - { - return false; - } - - return parsed.FullName == FullName; - case MTypeReferenced referenced: - if (NestedParent != null - && (referenced.ResolutionScope is not MResScopeType parentRes || - NestedParent.WhitelistEquals(parentRes.Type) == false)) - { - return false; - } - - var refFullName = referenced.Namespace == null - ? referenced.Name - : $"{referenced.Namespace}.{referenced.Name}"; - return FullName == refFullName; - default: - return false; - } - } - } - - internal sealed record MTypePrimitive(PrimitiveTypeCode TypeCode) : MType - { - public override string ToString() - { - return TypeCode switch - { - PrimitiveTypeCode.Void => "void", - PrimitiveTypeCode.Boolean => "bool", - PrimitiveTypeCode.Char => "char", - PrimitiveTypeCode.SByte => "int8", - PrimitiveTypeCode.Byte => "unsigned int8", - PrimitiveTypeCode.Int16 => "int16", - PrimitiveTypeCode.UInt16 => "unsigned int16", - PrimitiveTypeCode.Int32 => "int32", - PrimitiveTypeCode.UInt32 => "unsigned int32", - PrimitiveTypeCode.Int64 => "int64", - PrimitiveTypeCode.UInt64 => "unsigned int64", - PrimitiveTypeCode.Single => "float32", - PrimitiveTypeCode.Double => "float64", - PrimitiveTypeCode.String => "string", - // ReSharper disable once StringLiteralTypo - PrimitiveTypeCode.TypedReference => "typedref", - PrimitiveTypeCode.IntPtr => "native int", - PrimitiveTypeCode.UIntPtr => "unsigned native int", - PrimitiveTypeCode.Object => "object", - _ => "???" - }; - } - - public override string WhitelistToString() - { - return TypeCode switch - { - PrimitiveTypeCode.Void => "void", - PrimitiveTypeCode.Boolean => "bool", - PrimitiveTypeCode.Char => "char", - PrimitiveTypeCode.SByte => "sbyte", - PrimitiveTypeCode.Byte => "byte", - PrimitiveTypeCode.Int16 => "short", - PrimitiveTypeCode.UInt16 => "ushort", - PrimitiveTypeCode.Int32 => "int", - PrimitiveTypeCode.UInt32 => "uint", - PrimitiveTypeCode.Int64 => "long", - PrimitiveTypeCode.UInt64 => "ulong", - PrimitiveTypeCode.Single => "float", - PrimitiveTypeCode.Double => "double", - PrimitiveTypeCode.String => "string", - // ReSharper disable once StringLiteralTypo - PrimitiveTypeCode.TypedReference => "typedref", - // ReSharper disable once StringLiteralTypo - PrimitiveTypeCode.IntPtr => "nint", - // ReSharper disable once StringLiteralTypo - PrimitiveTypeCode.UIntPtr => "unint", - PrimitiveTypeCode.Object => "object", - _ => "???" - }; - } - - public override bool WhitelistEquals(MType other) - { - return Equals(other); - } - } - - // Normal single dimensional array with zero lower bound. - internal sealed record MTypeSZArray(MType ElementType) : MType - { - public override string ToString() - { - return $"{ElementType}[]"; - } - - public override string WhitelistToString() - { - return $"{ElementType.WhitelistToString()}[]"; - } - - public override bool WhitelistEquals(MType other) - { - return other is MTypeSZArray arr && ElementType.WhitelistEquals(arr.ElementType); - } - } - - // Multi-dimension arrays with funny lower and upper bounds. - internal sealed record MTypeWackyArray(MType ElementType, ArrayShape Shape) : MType - { - public override string ToString() - { - return $"{ElementType}[TODO]"; - } - - public override string WhitelistToString() - { - return $"{ElementType.WhitelistToString()}[TODO]"; - } - - public override bool WhitelistEquals(MType other) - { - return other is MTypeWackyArray arr && ShapesEqual(Shape, arr.Shape) && ElementType.WhitelistEquals(arr); - } - - private static bool ShapesEqual(in ArrayShape a, in ArrayShape b) - { - return a.Rank == b.Rank && a.LowerBounds.SequenceEqual(b.LowerBounds) && a.Sizes.SequenceEqual(b.Sizes); - } - - public override bool IsCoreTypeDefined() - { - return ElementType.IsCoreTypeDefined(); - } - } - - internal sealed record MTypeByRef(MType ElementType) : MType - { - public override string ToString() - { - return $"{ElementType}&"; - } - - public override string WhitelistToString() - { - return $"ref {ElementType.WhitelistToString()}"; - } - - public override bool WhitelistEquals(MType other) - { - return other is MTypeByRef byRef && ElementType.WhitelistEquals(byRef.ElementType); - } - } - - internal sealed record MTypePointer(MType ElementType) : MType - { - public override string ToString() - { - return $"{ElementType}*"; - } - - public override string WhitelistToString() - { - return $"{ElementType.WhitelistToString()}*"; - } - - public override bool WhitelistEquals(MType other) - { - return other is MTypePointer ptr && ElementType.WhitelistEquals(ptr.ElementType); - } - } - - internal sealed record MTypeGeneric(MType GenericType, ImmutableArray TypeArguments) : MType - { - public override string ToString() - { - return $"{GenericType}<{string.Join(", ", TypeArguments)}>"; - } - - public override string WhitelistToString() - { - return - $"{GenericType.WhitelistToString()}<{string.Join(", ", TypeArguments.Select(t => t.WhitelistToString()))}>"; - } - - public override bool WhitelistEquals(MType other) - { - if (!(other is MTypeGeneric generic)) - { - return false; - } - - if (TypeArguments.Length != generic.TypeArguments.Length) - { - return false; - } - - for (var i = 0; i < TypeArguments.Length; i++) - { - var argA = TypeArguments[i]; - var argB = generic.TypeArguments[i]; - - if (!argA.WhitelistEquals(argB)) - { - return false; - } - } - - return GenericType.WhitelistEquals(generic.GenericType); - } - - public bool Equals(MTypeGeneric? otherGeneric) - { - return otherGeneric != null && GenericType.Equals(otherGeneric.GenericType) && - TypeArguments.SequenceEqual(otherGeneric.TypeArguments); - } - - public override int GetHashCode() - { - var hc = new HashCode(); - hc.Add(GenericType); - foreach (var typeArg in TypeArguments) - { - hc.Add(typeArg); - } - - return hc.ToHashCode(); - } - - public override bool IsCoreTypeDefined() - { - return GenericType.IsCoreTypeDefined(); - } - } - - internal sealed record MTypeDefined(string Name, string? Namespace, MTypeDefined? Enclosing) : MType - { - public override string ToString() - { - var name = Namespace != null ? $"{Namespace}.{Name}" : Name; - - if (Enclosing != null) - { - return $"{Enclosing}/{name}"; - } - - return name; - } - - public override bool IsCoreTypeDefined() - { - return true; - } - } - - internal sealed record MTypeReferenced(MResScope ResolutionScope, string Name, string? Namespace) : MType - { - public override string ToString() - { - if (Namespace == null) - { - return $"{ResolutionScope}{Name}"; - } - - return $"{ResolutionScope}{Namespace}.{Name}"; - } - - public override string WhitelistToString() - { - if (Namespace == null) - { - return Name; - } - - return $"{Namespace}.{Name}"; - } - - public override bool WhitelistEquals(MType other) - { - return other switch - { - MTypeParsed p => p.WhitelistEquals(this), - // TODO: ResolutionScope doesn't actually implement equals - // This is fine since we're not comparing these anywhere - MTypeReferenced r => r.Namespace == Namespace && r.Name == Name && - r.ResolutionScope.Equals(ResolutionScope), - _ => false - }; - } - } - - public record MResScope - { - } - - internal sealed record MResScopeType(MType Type) : MResScope - { - public override string ToString() - { - return $"{Type}/"; - } - } - - internal sealed record MResScopeAssembly(string Name) : MResScope - { - public override string ToString() - { - return $"[{Name}]"; - } - } - - internal sealed record MTypeGenericTypePlaceHolder(int Index) : MType - { - public override string ToString() - { - return $"!{Index}"; - } - - public override bool WhitelistEquals(MType other) - { - return Equals(other); - } - } - - internal sealed record MTypeGenericMethodPlaceHolder(int Index) : MType - { - public override string ToString() - { - return $"!!{Index}"; - } - - public override bool WhitelistEquals(MType other) - { - return Equals(other); - } - } - - internal sealed record MTypeModified(MType UnmodifiedType, MType ModifierType, bool Required) : MType - { - public override string ToString() - { - var modName = Required ? "modreq" : "modopt"; - return $"{UnmodifiedType} {modName}({ModifierType})"; - } - - public override string? WhitelistToString() - { - return UnmodifiedType.WhitelistToString(); - } - - public override bool WhitelistEquals(MType other) - { - // TODO: This is asymmetric shit. - return UnmodifiedType.WhitelistEquals(other); - } - } -} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/TypeProvider.cs b/UnitystationLauncher/ContentScanning/TypeProvider.cs new file mode 100644 index 00000000..5483dc6b --- /dev/null +++ b/UnitystationLauncher/ContentScanning/TypeProvider.cs @@ -0,0 +1,83 @@ + +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +namespace UnitystationLauncher.ContentScanning; + +internal sealed class TypeProvider : ISignatureTypeProvider +{ + public MType GetSZArrayType(MType elementType) + { + return new MTypeSZArray(elementType); + } + + public MType GetArrayType(MType elementType, ArrayShape shape) + { + return new MTypeWackyArray(elementType, shape); + } + + public MType GetByReferenceType(MType elementType) + { + return new MTypeByRef(elementType); + } + + public MType GetGenericInstantiation(MType genericType, ImmutableArray typeArguments) + { + return new MTypeGeneric(genericType, typeArguments); + } + + public MType GetPointerType(MType elementType) + { + return new MTypePointer(elementType); + } + + public MType GetPrimitiveType(PrimitiveTypeCode typeCode) + { + return new MTypePrimitive(typeCode); + } + + public MType GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + return AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, handle); + } + + public MType GetTypeFromReference(MetadataReader inReader, TypeReferenceHandle inHandle, byte inRawTypeKind) + { + return AssemblyTypeCheckerHelpers.ParseTypeReference(inReader, inHandle); + } + + public MType GetFunctionPointerType(MethodSignature signature) + { + throw new NotImplementedException(); + } + + public MType GetGenericMethodParameter(int genericContext, int index) + { + return new MTypeGenericMethodPlaceHolder(index); + } + + public MType GetGenericTypeParameter(int genericContext, int index) + { + return new MTypeGenericTypePlaceHolder(index); + } + + public MType GetModifiedType(MType modifier, MType unmodifiedType, bool isRequired) + { + return new MTypeModified(unmodifiedType, modifier, isRequired); + } + + public MType GetPinnedType(MType elementType) + { + throw new NotImplementedException(); + } + + public MType GetTypeFromSpecification(MetadataReader reader, int genericContext, + TypeSpecificationHandle handle, + byte rawTypeKind) + { + return reader.GetTypeSpecification(handle).DecodeSignature(this, 0); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Exceptions/UnsupportedMetadataException.cs b/UnitystationLauncher/Exceptions/UnsupportedMetadataException.cs new file mode 100644 index 00000000..bfe77379 --- /dev/null +++ b/UnitystationLauncher/Exceptions/UnsupportedMetadataException.cs @@ -0,0 +1,19 @@ + +using System; + +namespace UnitystationLauncher.Exceptions; + +public sealed class UnsupportedMetadataException : Exception +{ + public UnsupportedMetadataException() + { + } + + public UnsupportedMetadataException(string message) : base(message) + { + } + + public UnsupportedMetadataException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs b/UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs new file mode 100644 index 00000000..71c040fd --- /dev/null +++ b/UnitystationLauncher/GameCommunicationPipe/PipeHubBuildCommunication.cs @@ -0,0 +1,127 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Reactive.Concurrency; +using System.Threading.Tasks; +using MessageBox.Avalonia.BaseWindows.Base; +using ReactiveUI; +using Serilog; +using UnitystationLauncher.Infrastructure; +using UnitystationLauncher.Models.Enums; + +namespace UnitystationLauncher.GameCommunicationPipe; + +public class PipeHubBuildCommunication : IDisposable +{ + private NamedPipeServerStream _serverPipe; + private StreamReader? _reader; + private StreamWriter? _writer; + + public PipeHubBuildCommunication() + { + _serverPipe = new NamedPipeServerStream("Unitystation_Hub_Build_Communication", PipeDirection.InOut, 1, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + } + + public async Task StartServerPipe() + { + await _serverPipe.WaitForConnectionAsync(); + _reader = new StreamReader(_serverPipe); + _writer = new StreamWriter(_serverPipe); + + while (true) + { + string? request = await _reader.ReadLineAsync(); + if (request == null) + { + try + { + await _serverPipe.WaitForConnectionAsync(); + } + catch (IOException e) + { + Log.Error(e.ToString()); + _serverPipe.Close(); + _serverPipe = new NamedPipeServerStream("Unitystation_Hub_Build_Communication", PipeDirection.InOut, + 1, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + await _serverPipe.WaitForConnectionAsync(); + } + + _reader = new StreamReader(_serverPipe); + _writer = new StreamWriter(_serverPipe); + continue; + } + + string[] requests = request.Split(","); + Log.Information($"Server: Received request: {request}"); + + if (ClientRequest.URL.ToString() == requests[0]) + { + RxApp.MainThreadScheduler.ScheduleAsync(async (_, _) => + { + IMsBoxWindow msgBox = MessageBoxBuilder.CreateMessageBox( + MessageBoxButtons.YesNo, + string.Empty, + $"would you like to add this Domain to The allowed domains to be opened In your browser, {requests[1]} " + + @" +Justification given by the Fork : " + requests[2]); + + string response = await msgBox.Show(); + Log.Information($"response {response}"); + await _writer.WriteLineAsync(response == "No" ? false.ToString() : true.ToString()); + await _writer.FlushAsync(); + return Task.CompletedTask; + }); + } + else if (ClientRequest.API_URL.ToString() == requests[0]) + { + RxApp.MainThreadScheduler.ScheduleAsync(async (_, _) => + { + IMsBoxWindow msgBox = MessageBoxBuilder.CreateMessageBox( + MessageBoxButtons.YesNo, + string.Empty, + $"The build would like to send an API request to, {requests[1]} " + @" +do you allow this fork to now on access this domain +Justification given by the Fork : " + requests[2]); + + + string response = await msgBox.Show(); + Log.Information($"response {response}"); + await _writer.WriteLineAsync(response == "No" ? false.ToString() : true.ToString()); + await _writer.FlushAsync(); + return Task.CompletedTask; + }); + } + else if (ClientRequest.Host_Trust_Mode.ToString() == requests[0]) + { + RxApp.MainThreadScheduler.ScheduleAsync(async (_, _) => + { + IMsBoxWindow msgBox = MessageBoxBuilder.CreateMessageBox( + MessageBoxButtons.YesNo, + string.Empty, + @" Trusted mode automatically allows every API and open URL action to happen without prompt, this also enables the +Variable viewer ( Application that can modify the games Data ) that Could potentially be used to Perform malicious actions on your PC, + The main purpose of this Prompt is to allow the Variable viewer (Variable editing), +What follows is given by the build, we do not control what is written in the Following text So treat with caution and use your brain + Justification : " + requests[1]); //TODO Add text + + string response = await msgBox.Show(); + Log.Information($"response {response}"); + await _writer.WriteLineAsync(response == "No" ? false.ToString() : true.ToString()); + await _writer.FlushAsync(); + return Task.CompletedTask; + }); + } + } + } + + public void Dispose() + { + _serverPipe.Dispose(); + _reader?.Dispose(); + _writer?.Dispose(); + + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Infrastructure/TypeExtensions.cs b/UnitystationLauncher/Infrastructure/TypeExtensions.cs new file mode 100644 index 00000000..b6b29725 --- /dev/null +++ b/UnitystationLauncher/Infrastructure/TypeExtensions.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Internal.TypeSystem.Ecma; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Models.ContentScanning; + +namespace UnitystationLauncher.Infrastructure; + +public static class TypeExtensions +{ + public static IEnumerable DumpMetaMembers(this Type type) + { + string assemblyLoc = type.Assembly.Location; + + // Load assembly with System.Reflection.Metadata. + using FileStream fs = File.OpenRead(assemblyLoc); + using PEReader peReader = new PEReader(fs); + + MetadataReader metaReader = peReader.GetMetadataReader(); + + // Find type definition in raw assembly metadata. + // Is there a better way to do this than iterating?? + TypeDefinition typeDef = default; + bool found = false; + foreach (TypeDefinitionHandle typeDefHandle in metaReader.TypeDefinitions) + { + TypeDefinition tempTypeDef = metaReader.GetTypeDefinition(typeDefHandle); + string name = metaReader.GetString(tempTypeDef.Name); + string? @namespace = AssemblyTypeCheckerHelpers.NilNullString(metaReader, tempTypeDef.Namespace); + if (name == type.Name && @namespace == type.Namespace) + { + typeDef = tempTypeDef; + found = true; + break; + } + } + + if (!found) + { + throw new InvalidOperationException("Type didn't exist??"); + } + + // Dump the list. + TypeProvider provider = new(); + + foreach (FieldDefinitionHandle fieldHandle in typeDef.GetFields()) + { + FieldDefinition fieldDef = metaReader.GetFieldDefinition(fieldHandle); + + if ((fieldDef.Attributes & FieldAttributes.FieldAccessMask) != FieldAttributes.Public) + { + continue; + } + + string fieldName = metaReader.GetString(fieldDef.Name); + MType fieldType = fieldDef.DecodeSignature(provider, 0); + + yield return $"{fieldType.WhitelistToString()} {fieldName}"; + } + + foreach (MethodDefinitionHandle methodHandle in typeDef.GetMethods()) + { + MethodDefinition methodDef = metaReader.GetMethodDefinition(methodHandle); + + if (!methodDef.Attributes.IsPublic()) + { + continue; + } + + string methodName = metaReader.GetString(methodDef.Name); + MethodSignature methodSig = methodDef.DecodeSignature(provider, 0); + + string paramString = string.Join(", ", methodSig.ParameterTypes.Select(t => t.WhitelistToString())); + int genericCount = methodSig.GenericParameterCount; + string typeParamString = genericCount == 0 + ? "" + : $"<{new string(',', genericCount - 1)}>"; + + yield return $"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})"; + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ConfigFile/HubClientConfig.cs b/UnitystationLauncher/Models/ConfigFile/HubClientConfig.cs index c3c84f15..afc9a280 100644 --- a/UnitystationLauncher/Models/ConfigFile/HubClientConfig.cs +++ b/UnitystationLauncher/Models/ConfigFile/HubClientConfig.cs @@ -1,29 +1,27 @@ using System; -using System.Runtime.InteropServices; using UnitystationLauncher.Models.Enums; using UnitystationLauncher.Services.Interface; -namespace UnitystationLauncher.Models.ConfigFile +namespace UnitystationLauncher.Models.ConfigFile; + +[Serializable] +public class HubClientConfig { - [Serializable] - public class HubClientConfig - { - public int? BuildNumber { get; set; } - public string? WinUrl { get; set; } - public string? OsxUrl { get; set; } - public string? LinuxUrl { get; set; } - public string? DailyMessage { get; set; } + public int? BuildNumber { get; set; } + public string? WinUrl { get; set; } + public string? OsxUrl { get; set; } + public string? LinuxUrl { get; set; } + public string? DailyMessage { get; set; } - public string? GetDownloadUrl(IEnvironmentService environmentService) + public string? GetDownloadUrl(IEnvironmentService environmentService) + { + return environmentService.GetCurrentEnvironment() switch { - return environmentService.GetCurrentEnvironment() switch - { - CurrentEnvironment.WindowsStandalone => WinUrl, - CurrentEnvironment.MacOsStandalone => OsxUrl, - CurrentEnvironment.LinuxStandalone - or CurrentEnvironment.LinuxFlatpak => LinuxUrl, - _ => null - }; - } + CurrentEnvironment.WindowsStandalone => WinUrl, + CurrentEnvironment.MacOsStandalone => OsxUrl, + CurrentEnvironment.LinuxStandalone + or CurrentEnvironment.LinuxFlatpak => LinuxUrl, + _ => null + }; } -} \ No newline at end of file +} diff --git a/UnitystationLauncher/Models/ConfigFile/Preferences.cs b/UnitystationLauncher/Models/ConfigFile/Preferences.cs index 67479846..7f9b340f 100644 --- a/UnitystationLauncher/Models/ConfigFile/Preferences.cs +++ b/UnitystationLauncher/Models/ConfigFile/Preferences.cs @@ -1,29 +1,28 @@ using ReactiveUI; -namespace UnitystationLauncher.Models.ConfigFile +namespace UnitystationLauncher.Models.ConfigFile; + +public class Preferences : ReactiveObject { - public class Preferences : ReactiveObject - { - private bool _autoRemove = true; - private int _ignoreVersionUpdate; - private string _installationPath = string.Empty; + private bool _autoRemove = true; + private int _ignoreVersionUpdate; + private string _installationPath = string.Empty; - public bool AutoRemove - { - get => _autoRemove; - set => this.RaiseAndSetIfChanged(ref _autoRemove, value); - } + public bool AutoRemove + { + get => _autoRemove; + set => this.RaiseAndSetIfChanged(ref _autoRemove, value); + } - public int IgnoreVersionUpdate - { - get => _ignoreVersionUpdate; - set => this.RaiseAndSetIfChanged(ref _ignoreVersionUpdate, value); - } + public int IgnoreVersionUpdate + { + get => _ignoreVersionUpdate; + set => this.RaiseAndSetIfChanged(ref _ignoreVersionUpdate, value); + } - public string InstallationPath - { - get => _installationPath; - set => this.RaiseAndSetIfChanged(ref _installationPath, value); - } + public string InstallationPath + { + get => _installationPath; + set => this.RaiseAndSetIfChanged(ref _installationPath, value); } -} \ No newline at end of file +} diff --git a/UnitystationLauncher/ContentScanning/ScanConfigMode.cs b/UnitystationLauncher/Models/ConfigFile/SandboxConfig.cs similarity index 80% rename from UnitystationLauncher/ContentScanning/ScanConfigMode.cs rename to UnitystationLauncher/Models/ConfigFile/SandboxConfig.cs index a9c91ab6..47182577 100644 --- a/UnitystationLauncher/ContentScanning/ScanConfigMode.cs +++ b/UnitystationLauncher/Models/ConfigFile/SandboxConfig.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; -using System.Globalization; using ILVerify; +using UnitystationLauncher.Models.ContentScanning; -namespace UnitystationLauncher.ContentScanning; +namespace UnitystationLauncher.Models.ConfigFile; +[Serializable] public sealed class SandboxConfig { public string? SystemAssemblyName { get; set; } @@ -14,4 +15,4 @@ public sealed class SandboxConfig public Dictionary> Types { get; set; } = new Dictionary>(); -} +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/MType.cs b/UnitystationLauncher/Models/ContentScanning/MType.cs similarity index 87% rename from UnitystationLauncher/ContentScanning/MType.cs rename to UnitystationLauncher/Models/ContentScanning/MType.cs index ee2e0686..b56b42fe 100644 --- a/UnitystationLauncher/ContentScanning/MType.cs +++ b/UnitystationLauncher/Models/ContentScanning/MType.cs @@ -1,4 +1,4 @@ -namespace UnitystationLauncher.ContentScanning; +namespace UnitystationLauncher.Models.ContentScanning; public record MType diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRef.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRef.cs new file mode 100644 index 00000000..268e00df --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRef.cs @@ -0,0 +1,13 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +public class MMemberRef +{ + public readonly MType ParentType; + public readonly string Name; + + protected MMemberRef(MType parentType, string name) + { + ParentType = parentType; + Name = name; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefField.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefField.cs new file mode 100644 index 00000000..aa335591 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefField.cs @@ -0,0 +1,16 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed class MMemberRefField : MMemberRef +{ + internal readonly MType FieldType; + + public MMemberRefField(MType parentType, string name, MType fieldType) : base(parentType, name) + { + FieldType = fieldType; + } + + public override string ToString() + { + return $"{ParentType}.{Name} Returns {FieldType}"; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefMethod.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefMethod.cs new file mode 100644 index 00000000..7e728bcc --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MMemberRefMethod.cs @@ -0,0 +1,23 @@ +using System.Collections.Immutable; + +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed class MMemberRefMethod : MMemberRef +{ + public readonly MType ReturnType; + public readonly int GenericParameterCount; + public readonly ImmutableArray ParameterTypes; + + public MMemberRefMethod(MType parentType, string name, MType returnType, + int genericParameterCount, ImmutableArray parameterTypes) : base(parentType, name) + { + ReturnType = returnType; + GenericParameterCount = genericParameterCount; + ParameterTypes = parameterTypes; + } + + public override string ToString() + { + return $"{ParentType}.{Name}({string.Join(", ", ParameterTypes)}) Returns {ReturnType}"; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScope.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScope.cs new file mode 100644 index 00000000..4f57f210 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScope.cs @@ -0,0 +1,5 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +public record MResScope +{ +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeAssembly.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeAssembly.cs new file mode 100644 index 00000000..ccacbf7b --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeAssembly.cs @@ -0,0 +1,9 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MResScopeAssembly(string Name) : MResScope +{ + public override string ToString() + { + return $"[{Name}]"; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeType.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeType.cs new file mode 100644 index 00000000..b906240e --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MResScopeType.cs @@ -0,0 +1,10 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + + +internal sealed record MResScopeType(MType Type) : MResScope +{ + public override string ToString() + { + return $"{Type}/"; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeByRef.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeByRef.cs new file mode 100644 index 00000000..5091e3d6 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeByRef.cs @@ -0,0 +1,19 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeByRef(MType ElementType) : MType +{ + public override string ToString() + { + return $"{ElementType}&"; + } + + public override string WhitelistToString() + { + return $"ref {ElementType.WhitelistToString()}"; + } + + public override bool WhitelistEquals(MType other) + { + return other is MTypeByRef byRef && ElementType.WhitelistEquals(byRef.ElementType); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeDefined.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeDefined.cs new file mode 100644 index 00000000..47819126 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeDefined.cs @@ -0,0 +1,21 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeDefined(string Name, string? Namespace, MTypeDefined? Enclosing) : MType +{ + public override string ToString() + { + string name = Namespace != null ? $"{Namespace}.{Name}" : Name; + + if (Enclosing != null) + { + return $"{Enclosing}/{name}"; + } + + return name; + } + + public override bool IsCoreTypeDefined() + { + return true; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs new file mode 100644 index 00000000..6bc8a848 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGeneric.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Immutable; +using System.Linq; + +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeGeneric(MType GenericType, ImmutableArray TypeArguments) : MType +{ + public override string ToString() + { + return $"{GenericType}<{string.Join(", ", TypeArguments)}>"; + } + + public override string WhitelistToString() + { + return + $"{GenericType.WhitelistToString()}<{string.Join(", ", TypeArguments.Select(t => t.WhitelistToString()))}>"; + } + + public override bool WhitelistEquals(MType other) + { + if (!(other is MTypeGeneric generic)) + { + return false; + } + + if (TypeArguments.Length != generic.TypeArguments.Length) + { + return false; + } + + for (int i = 0; i < TypeArguments.Length; i++) + { + MType argA = TypeArguments[i]; + MType argB = generic.TypeArguments[i]; + + if (!argA.WhitelistEquals(argB)) + { + return false; + } + } + + return GenericType.WhitelistEquals(generic.GenericType); + } + + public bool Equals(MTypeGeneric? otherGeneric) + { + return otherGeneric != null && GenericType.Equals(otherGeneric.GenericType) && + TypeArguments.SequenceEqual(otherGeneric.TypeArguments); + } + + public override int GetHashCode() + { + HashCode hc = new HashCode(); + hc.Add(GenericType); + foreach (MType typeArg in TypeArguments) + { + hc.Add(typeArg); + } + + return hc.ToHashCode(); + } + + public override bool IsCoreTypeDefined() + { + return GenericType.IsCoreTypeDefined(); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericMethodPlaceHolder.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericMethodPlaceHolder.cs new file mode 100644 index 00000000..17d78835 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericMethodPlaceHolder.cs @@ -0,0 +1,14 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeGenericMethodPlaceHolder(int Index) : MType +{ + public override string ToString() + { + return $"!!{Index}"; + } + + public override bool WhitelistEquals(MType other) + { + return Equals(other); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericTypePlaceHolder.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericTypePlaceHolder.cs new file mode 100644 index 00000000..0da30605 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeGenericTypePlaceHolder.cs @@ -0,0 +1,14 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeGenericTypePlaceHolder(int Index) : MType +{ + public override string ToString() + { + return $"!{Index}"; + } + + public override bool WhitelistEquals(MType other) + { + return Equals(other); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeModified.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeModified.cs new file mode 100644 index 00000000..cd55f07c --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeModified.cs @@ -0,0 +1,21 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeModified(MType UnmodifiedType, MType ModifierType, bool Required) : MType +{ + public override string ToString() + { + string modName = Required ? "modreq" : "modopt"; + return $"{UnmodifiedType} {modName}({ModifierType})"; + } + + public override string? WhitelistToString() + { + return UnmodifiedType.WhitelistToString(); + } + + public override bool WhitelistEquals(MType other) + { + // TODO: This is asymmetric shit. + return UnmodifiedType.WhitelistEquals(other); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeParsed.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeParsed.cs new file mode 100644 index 00000000..e51b8591 --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeParsed.cs @@ -0,0 +1,38 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeParsed(string FullName, MTypeParsed? NestedParent = null) : MType +{ + public override string ToString() + { + return NestedParent != null ? $"{NestedParent}/{FullName}" : FullName; + } + + public override bool WhitelistEquals(MType other) + { + switch (other) + { + case MTypeParsed parsed: + if (NestedParent != null + && (parsed.NestedParent == null || NestedParent.WhitelistEquals(parsed.NestedParent) == false)) + { + return false; + } + + return parsed.FullName == FullName; + case MTypeReferenced referenced: + if (NestedParent != null + && (referenced.ResolutionScope is not MResScopeType parentRes || + NestedParent.WhitelistEquals(parentRes.Type) == false)) + { + return false; + } + + string refFullName = referenced.Namespace == null + ? referenced.Name + : $"{referenced.Namespace}.{referenced.Name}"; + return FullName == refFullName; + default: + return false; + } + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePointer.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePointer.cs new file mode 100644 index 00000000..3711d5ae --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePointer.cs @@ -0,0 +1,19 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypePointer(MType ElementType) : MType +{ + public override string ToString() + { + return $"{ElementType}*"; + } + + public override string WhitelistToString() + { + return $"{ElementType.WhitelistToString()}*"; + } + + public override bool WhitelistEquals(MType other) + { + return other is MTypePointer ptr && ElementType.WhitelistEquals(ptr.ElementType); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePrimitive.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePrimitive.cs new file mode 100644 index 00000000..f994ddab --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypePrimitive.cs @@ -0,0 +1,67 @@ +using System.Reflection.Metadata; + +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypePrimitive(PrimitiveTypeCode TypeCode) : MType +{ + public override string ToString() + { + return TypeCode switch + { + PrimitiveTypeCode.Void => "void", + PrimitiveTypeCode.Boolean => "bool", + PrimitiveTypeCode.Char => "char", + PrimitiveTypeCode.SByte => "int8", + PrimitiveTypeCode.Byte => "unsigned int8", + PrimitiveTypeCode.Int16 => "int16", + PrimitiveTypeCode.UInt16 => "unsigned int16", + PrimitiveTypeCode.Int32 => "int32", + PrimitiveTypeCode.UInt32 => "unsigned int32", + PrimitiveTypeCode.Int64 => "int64", + PrimitiveTypeCode.UInt64 => "unsigned int64", + PrimitiveTypeCode.Single => "float32", + PrimitiveTypeCode.Double => "float64", + PrimitiveTypeCode.String => "string", + // ReSharper disable once StringLiteralTypo + PrimitiveTypeCode.TypedReference => "typedref", + PrimitiveTypeCode.IntPtr => "native int", + PrimitiveTypeCode.UIntPtr => "unsigned native int", + PrimitiveTypeCode.Object => "object", + _ => "???" + }; + } + + public override string WhitelistToString() + { + return TypeCode switch + { + PrimitiveTypeCode.Void => "void", + PrimitiveTypeCode.Boolean => "bool", + PrimitiveTypeCode.Char => "char", + PrimitiveTypeCode.SByte => "sbyte", + PrimitiveTypeCode.Byte => "byte", + PrimitiveTypeCode.Int16 => "short", + PrimitiveTypeCode.UInt16 => "ushort", + PrimitiveTypeCode.Int32 => "int", + PrimitiveTypeCode.UInt32 => "uint", + PrimitiveTypeCode.Int64 => "long", + PrimitiveTypeCode.UInt64 => "ulong", + PrimitiveTypeCode.Single => "float", + PrimitiveTypeCode.Double => "double", + PrimitiveTypeCode.String => "string", + // ReSharper disable once StringLiteralTypo + PrimitiveTypeCode.TypedReference => "typedref", + // ReSharper disable once StringLiteralTypo + PrimitiveTypeCode.IntPtr => "nint", + // ReSharper disable once StringLiteralTypo + PrimitiveTypeCode.UIntPtr => "unint", + PrimitiveTypeCode.Object => "object", + _ => "???" + }; + } + + public override bool WhitelistEquals(MType other) + { + return Equals(other); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs new file mode 100644 index 00000000..d244824f --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeReferenced.cs @@ -0,0 +1,37 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +internal sealed record MTypeReferenced(MResScope ResolutionScope, string Name, string? Namespace) : MType +{ + public override string ToString() + { + if (Namespace == null) + { + return $"{ResolutionScope}{Name}"; + } + + return $"{ResolutionScope}{Namespace}.{Name}"; + } + + public override string WhitelistToString() + { + if (Namespace == null) + { + return Name; + } + + return $"{Namespace}.{Name}"; + } + + public override bool WhitelistEquals(MType other) + { + return other switch + { + MTypeParsed p => p.WhitelistEquals(this), + // TODO: ResolutionScope doesn't actually implement equals + // This is fine since we're not comparing these anywhere + MTypeReferenced r => r.Namespace == Namespace && r.Name == Name && + r.ResolutionScope.Equals(ResolutionScope), + _ => false + }; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs new file mode 100644 index 00000000..1b2a4d3e --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeSZArray.cs @@ -0,0 +1,20 @@ +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +// Normal single dimensional array with zero lower bound. +internal sealed record MTypeSZArray(MType ElementType) : MType +{ + public override string ToString() + { + return $"{ElementType}[]"; + } + + public override string WhitelistToString() + { + return $"{ElementType.WhitelistToString()}[]"; + } + + public override bool WhitelistEquals(MType other) + { + return other is MTypeSZArray arr && ElementType.WhitelistEquals(arr.ElementType); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs new file mode 100644 index 00000000..5aa08cbd --- /dev/null +++ b/UnitystationLauncher/Models/ContentScanning/ScanningTypes/MTypeWackyArray.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Reflection.Metadata; + +namespace UnitystationLauncher.Models.ContentScanning.ScanningTypes; + +// Multi-dimension arrays with funny lower and upper bounds. +internal sealed record MTypeWackyArray(MType ElementType, ArrayShape Shape) : MType +{ + public override string ToString() + { + return $"{ElementType}[TODO]"; + } + + public override string WhitelistToString() + { + return $"{ElementType.WhitelistToString()}[TODO]"; + } + + public override bool WhitelistEquals(MType other) + { + return other is MTypeWackyArray arr && ShapesEqual(Shape, arr.Shape) && ElementType.WhitelistEquals(arr); + } + + private static bool ShapesEqual(in ArrayShape a, in ArrayShape b) + { + return a.Rank == b.Rank && a.LowerBounds.SequenceEqual(b.LowerBounds) && a.Sizes.SequenceEqual(b.Sizes); + } + + public override bool IsCoreTypeDefined() + { + return ElementType.IsCoreTypeDefined(); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/ContentScanning/TypeConfig.cs b/UnitystationLauncher/Models/ContentScanning/TypeConfig.cs similarity index 87% rename from UnitystationLauncher/ContentScanning/TypeConfig.cs rename to UnitystationLauncher/Models/ContentScanning/TypeConfig.cs index 9ada216d..3c8e2b5f 100644 --- a/UnitystationLauncher/ContentScanning/TypeConfig.cs +++ b/UnitystationLauncher/Models/ContentScanning/TypeConfig.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; +using UnitystationLauncher.Models.Enums; -namespace UnitystationLauncher.ContentScanning; +namespace UnitystationLauncher.Models.ContentScanning; public sealed class TypeConfig { diff --git a/UnitystationLauncher/ContentScanning/WhitelistFieldDefine.cs b/UnitystationLauncher/Models/ContentScanning/WhitelistFieldDefine.cs similarity index 80% rename from UnitystationLauncher/ContentScanning/WhitelistFieldDefine.cs rename to UnitystationLauncher/Models/ContentScanning/WhitelistFieldDefine.cs index ba1683b8..ba14d4b6 100644 --- a/UnitystationLauncher/ContentScanning/WhitelistFieldDefine.cs +++ b/UnitystationLauncher/Models/ContentScanning/WhitelistFieldDefine.cs @@ -1,4 +1,4 @@ -namespace UnitystationLauncher.ContentScanning; +namespace UnitystationLauncher.Models.ContentScanning; public sealed class WhitelistFieldDefine { diff --git a/UnitystationLauncher/ContentScanning/WhitelistMethodDefine.cs b/UnitystationLauncher/Models/ContentScanning/WhitelistMethodDefine.cs similarity index 91% rename from UnitystationLauncher/ContentScanning/WhitelistMethodDefine.cs rename to UnitystationLauncher/Models/ContentScanning/WhitelistMethodDefine.cs index 918fe4b4..4853efe7 100644 --- a/UnitystationLauncher/ContentScanning/WhitelistMethodDefine.cs +++ b/UnitystationLauncher/Models/ContentScanning/WhitelistMethodDefine.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace UnitystationLauncher.ContentScanning; +namespace UnitystationLauncher.Models.ContentScanning; public sealed class WhitelistMethodDefine { diff --git a/UnitystationLauncher/Models/Enums/ClientRequest.cs b/UnitystationLauncher/Models/Enums/ClientRequest.cs new file mode 100644 index 00000000..fb7f1d72 --- /dev/null +++ b/UnitystationLauncher/Models/Enums/ClientRequest.cs @@ -0,0 +1,8 @@ +namespace UnitystationLauncher.Models.Enums; + +internal enum ClientRequest +{ + URL = 1, + API_URL = 2, + Host_Trust_Mode = 3, +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/Enums/DumpFlags.cs b/UnitystationLauncher/Models/Enums/DumpFlags.cs new file mode 100644 index 00000000..395ace56 --- /dev/null +++ b/UnitystationLauncher/Models/Enums/DumpFlags.cs @@ -0,0 +1,14 @@ +using System; + +namespace UnitystationLauncher.Models.Enums; + +[Flags] +public enum DumpFlags : byte +{ + None = 0, + Types = 1, + Members = 2, + Inheritance = 4, + + All = Types | Members | Inheritance +} \ No newline at end of file diff --git a/UnitystationLauncher/Models/Enums/InheritMode.cs b/UnitystationLauncher/Models/Enums/InheritMode.cs index 14489cae..42468529 100644 --- a/UnitystationLauncher/Models/Enums/InheritMode.cs +++ b/UnitystationLauncher/Models/Enums/InheritMode.cs @@ -1,4 +1,4 @@ -namespace UnitystationLauncher.ContentScanning; +namespace UnitystationLauncher.Models.Enums; public enum InheritMode : byte { diff --git a/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs b/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs new file mode 100644 index 00000000..49fa87e2 --- /dev/null +++ b/UnitystationLauncher/Services/AssemblyTypeCheckerService.cs @@ -0,0 +1,623 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Threading.Tasks; +using ILVerify; +using MoreLinq; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Models.ConfigFile; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.ContentScanning.ScanningTypes; +using UnitystationLauncher.Models.Enums; +using UnitystationLauncher.Services.Interface; + +// psst +// You know ECMA-335 right? The specification for the CLI that .NET runs on? +// Yeah, you need it to understand a lot of this code. So get a copy. +// You know the cool thing? +// ISO has a version that has correct PDF metadata so there's an actual table of contents. +// Right here: https://standards.iso.org/ittf/PubliclyAvailableStandards/c058046_ISO_IEC_23271_2012(E).zip + +namespace UnitystationLauncher.Services; + +/// +/// Manages the type white/black list of types and namespaces, and verifies assemblies against them. +/// +public sealed partial class AssemblyTypeCheckerService : IAssemblyTypeCheckerService +{ + /// + /// Completely disables type checking, allowing everything. + /// + public bool DisableTypeCheck { get; init; } + + public DumpFlags Dump { get; init; } = DumpFlags.None; + public bool VerifyIl { get; init; } + + private readonly Task _config; + + private readonly IEnvironmentService _environmentService; + + + private readonly ICodeScanConfigService _codeScanConfigService; + + private readonly HttpClient _httpClient; + + public AssemblyTypeCheckerService(IEnvironmentService environmentService, HttpClient httpClient, ICodeScanConfigService codeScanConfigService) + { + _environmentService = environmentService; + VerifyIl = true; + DisableTypeCheck = false; + _httpClient = httpClient; + _codeScanConfigService = codeScanConfigService; + _config = Task.Run(_codeScanConfigService.LoadConfigAsync); + } + + /// + /// Check the assembly for any illegal types. Any types not on the white list + /// will cause the assembly to be rejected. + /// + /// + /// + /// + /// + /// + /// Assembly to load. + /// + public bool CheckAssembly(FileInfo diskPath, DirectoryInfo managedPath, List otherAssemblies, + Action info, Action Errors) + { + using FileStream assembly = diskPath.OpenRead(); + Stopwatch fullStopwatch = Stopwatch.StartNew(); + + Resolver resolver = AssemblyTypeCheckerHelpers.CreateResolver(managedPath); + using PEReader peReader = new PEReader(assembly, PEStreamOptions.LeaveOpen); + MetadataReader reader = peReader.GetMetadataReader(); + + string asmName = reader.GetString(reader.GetAssemblyDefinition().Name); + + if (peReader.PEHeaders.CorHeader?.ManagedNativeHeaderDirectory is { Size: not 0 }) + { + Errors.Invoke($"Assembly {asmName} contains native code."); + return false; + } + + if (VerifyIl) + { + if (DoVerifyIL(asmName, resolver, peReader, reader, info, Errors) == false) + { + Errors.Invoke($"Assembly {asmName} Has invalid IL code"); + return false; + } + } + + + ConcurrentBag errors = new ConcurrentBag(); + + List types = AssemblyTypeCheckerHelpers.GetReferencedTypes(reader, errors); + List members = AssemblyTypeCheckerHelpers.GetReferencedMembers(reader, errors); + List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited = GetExternalInheritedTypes(reader, errors); + info.Invoke($"References loaded... {fullStopwatch.ElapsedMilliseconds}ms"); + + if (DisableTypeCheck) + { + resolver.Dispose(); + peReader.Dispose(); + return true; + } + + + SandboxConfig loadedConfig = _config.Result; + + loadedConfig.MultiAssemblyOtherReferences.Clear(); + loadedConfig.MultiAssemblyOtherReferences.AddRange(otherAssemblies); + + // We still do explicit type reference scanning, even though the actual whitelists work with raw members. + // This is so that we can simplify handling of generic type specifications during member checking: + // we won't have to check that any types in their type arguments are whitelisted. + foreach (MTypeReferenced type in types) + { + if (IsTypeAccessAllowed(loadedConfig, type, out _) == false) + { + errors.Add(new SandboxError($"Access to type not allowed: {type} asmName {asmName}")); + } + } + + info.Invoke($"Types... {fullStopwatch.ElapsedMilliseconds}ms"); + + CheckInheritance(loadedConfig, inherited, errors); + + info.Invoke($"Inheritance... {fullStopwatch.ElapsedMilliseconds}ms"); + + AssemblyTypeCheckerHelpers.CheckNoUnmanagedMethodDefs(reader, errors); + + info.Invoke($"Unmanaged methods... {fullStopwatch.ElapsedMilliseconds}ms"); + + AssemblyTypeCheckerHelpers.CheckNoTypeAbuse(reader, errors); + + info.Invoke($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms"); + + CheckMemberReferences(loadedConfig, members, errors); + + errors = new ConcurrentBag(errors.OrderBy(x => x.Message)); + + foreach (SandboxError error in errors) + { + Errors.Invoke($"Sandbox violation: {error.Message}"); + } + + info.Invoke($"Checked assembly in {fullStopwatch.ElapsedMilliseconds}ms"); + resolver.Dispose(); + peReader.Dispose(); + return errors.IsEmpty; + } + + private bool DoVerifyIL( + string name, + IResolver resolver, + PEReader peReader, + MetadataReader reader, + Action info, + Action logErrors) + { + info.Invoke($"{name}: Verifying IL..."); + Stopwatch sw = Stopwatch.StartNew(); + ConcurrentBag bag = new ConcurrentBag(); + + + bool UesParallel = false; + + if (UesParallel) + { + OrderablePartitioner partitioner = Partitioner.Create(reader.TypeDefinitions); + Parallel.ForEach(partitioner.GetPartitions(Environment.ProcessorCount), handle => + { + Verifier ver = new Verifier(resolver); + ver.SetSystemModuleName(new AssemblyName(AssemblyTypeCheckerHelpers.SystemAssemblyName)); + while (handle.MoveNext()) + { + foreach (VerificationResult? result in ver.Verify(peReader, handle.Current, verifyMethods: true)) + { + bag.Add(result); + } + } + }); + } + else + { + Verifier ver = new Verifier(resolver); + //mscorlib + ver.SetSystemModuleName(new AssemblyName(AssemblyTypeCheckerHelpers.SystemAssemblyName)); + foreach (TypeDefinitionHandle Definition in reader.TypeDefinitions) + { + IEnumerable Errors = ver.Verify(peReader, Definition, verifyMethods: true); + foreach (VerificationResult? Error in Errors) + { + bag.Add(Error); + } + } + } + + SandboxConfig loadedCfg = _config.Result; + + bool verifyErrors = false; + foreach (VerificationResult res in bag) + { + if (loadedCfg.AllowedVerifierErrors.Contains(res.Code)) + { + continue; + } + + string formatted = res.Args == null ? res.Message : string.Format(res.Message, res.Args); + string msg = $"{name}: ILVerify: {formatted}"; + + if (!res.Method.IsNil) + { + MethodDefinition method = reader.GetMethodDefinition(res.Method); + string methodName = AssemblyTypeCheckerHelpers.FormatMethodName(reader, method); + + msg = $"{msg}, method: {methodName}"; + } + + if (!res.Type.IsNil) + { + MTypeDefined type = AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, res.Type); + msg = $"{msg}, type: {type}"; + } + + + verifyErrors = true; + logErrors.Invoke(msg); + } + + info.Invoke($"{name}: Verified IL in {sw.Elapsed.TotalMilliseconds}ms"); + + if (verifyErrors) + { + return false; + } + + return true; + } + + private void CheckMemberReferences( + SandboxConfig sandboxConfig, + List members, + ConcurrentBag errors) + { + bool IsParallel = true; + + if (IsParallel) + { + Parallel.ForEach(members, memberRef => + { + MType baseType = memberRef.ParentType; + while (!(baseType is MTypeReferenced)) + { + switch (baseType) + { + case MTypeGeneric generic: + { + baseType = generic.GenericType; + + break; + } + case MTypeWackyArray: + { + // Members on arrays (not to be confused with vectors) are all fine. + // See II.14.2 in ECMA-335. + return; + } + case MTypeDefined: + { + // Valid for this to show up, safe to ignore. + return; + } + default: + { + throw new ArgumentOutOfRangeException(); + } + } + } + + MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; + + if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out TypeConfig? typeCfg) == false) + { + // Technically this error isn't necessary since we have an earlier pass + // checking all referenced types. That should have caught this + // We still need the typeCfg so that's why we're checking. Might as well. + errors.Add(new SandboxError($"Access to type not allowed: {baseTypeReferenced}")); + return; + } + + if (typeCfg.All) + { + // Fully whitelisted for the type, we good. + return; + } + + switch (memberRef) + { + case MMemberRefField mMemberRefField: + { + foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) + { + if (field.Name == mMemberRefField.Name && + mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) + { + return; // Found + } + } + + errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}")); + break; + } + case MMemberRefMethod mMemberRefMethod: + foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) + { + bool notParamMismatch = true; + + if (parsed.Name == mMemberRefMethod.Name && + mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && + mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && + mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) + { + for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) + { + MType a = mMemberRefMethod.ParameterTypes[i]; + MType b = parsed.ParameterTypes[i]; + + if (a.WhitelistEquals(b) == false) + { + notParamMismatch = false; + break; + } + } + + if (notParamMismatch) + { + return; // Found + } + } + } + + errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}")); + break; + default: + throw new ArgumentOutOfRangeException(nameof(memberRef)); + } + }); + } + else + { + foreach (MMemberRef memberRef in members) + { + MType baseType = memberRef.ParentType; + while (!(baseType is MTypeReferenced)) + { + switch (baseType) + { + case MTypeGeneric generic: + { + baseType = generic.GenericType; + + break; + } + case MTypeWackyArray: + { + // Members on arrays (not to be confused with vectors) are all fine. + // See II.14.2 in ECMA-335. + continue; + } + case MTypeDefined: + { + // Valid for this to show up, safe to ignore. + continue; + } + default: + { + throw new ArgumentOutOfRangeException(); + } + } + } + + MTypeReferenced baseTypeReferenced = (MTypeReferenced)baseType; + + if (IsTypeAccessAllowed(sandboxConfig, baseTypeReferenced, out TypeConfig? typeCfg) == false) + { + // Technically this error isn't necessary since we have an earlier pass + // checking all referenced types. That should have caught this + // We still need the typeCfg so that's why we're checking. Might as well. + errors.Add(new SandboxError($"Access to type not allowed: {baseTypeReferenced}")); + continue; + } + + if (typeCfg.All) + { + // Fully whitelisted for the type, we good. + continue; + } + + switch (memberRef) + { + case MMemberRefField mMemberRefField: + { + foreach (WhitelistFieldDefine field in typeCfg.FieldsParsed) + { + if (field.Name == mMemberRefField.Name && + mMemberRefField.FieldType.WhitelistEquals(field.FieldType)) + { + continue; // Found + } + } + + errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}")); + break; + } + case MMemberRefMethod mMemberRefMethod: + bool notParamMismatch = true; + foreach (WhitelistMethodDefine parsed in typeCfg.MethodsParsed) + { + if (parsed.Name == mMemberRefMethod.Name && + mMemberRefMethod.ReturnType.WhitelistEquals(parsed.ReturnType) && + mMemberRefMethod.ParameterTypes.Length == parsed.ParameterTypes.Count && + mMemberRefMethod.GenericParameterCount == parsed.GenericParameterCount) + { + for (int i = 0; i < mMemberRefMethod.ParameterTypes.Length; i++) + { + MType a = mMemberRefMethod.ParameterTypes[i]; + MType b = parsed.ParameterTypes[i]; + + if (!a.WhitelistEquals(b)) + { + notParamMismatch = false; + break; + + } + } + + if (notParamMismatch) + { + break; // Found + } + break; + } + } + + if (notParamMismatch == false) + { + continue; + } + + errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}")); + break; + default: + throw new ArgumentOutOfRangeException(nameof(memberRef)); + } + } + } + } + + private void CheckInheritance( + SandboxConfig sandboxConfig, + List<(MType type, MType parent, ArraySegment interfaceImpls)> inherited, + ConcurrentBag errors) + { + // This inheritance whitelisting primarily serves to avoid content doing funny stuff + // by e.g. inheriting Type. + foreach ((MType _, MType baseType, ArraySegment interfaces) in inherited) + { + if (CanInherit(baseType) == false) + { + errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType}")); + } + + foreach (MType @interface in interfaces) + { + if (CanInherit(@interface) == false) + { + errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface}")); + } + } + + bool CanInherit(MType inheritType) + { + MTypeReferenced realBaseType = inheritType switch + { + MTypeGeneric generic => (MTypeReferenced)generic.GenericType, + MTypeReferenced referenced => referenced, + _ => throw new InvalidOperationException() // Can't happen. + }; + + if (IsTypeAccessAllowed(sandboxConfig, realBaseType, out TypeConfig? cfg) == false) + { + return false; + } + + return cfg.Inherit != InheritMode.Block && (cfg.Inherit == InheritMode.Allow || cfg.All); + } + } + } + + private bool IsTypeAccessAllowed(SandboxConfig sandboxConfig, MTypeReferenced type, + [NotNullWhen(true)] out TypeConfig? cfg) + { + if (type.Namespace == null) + { + if (type.ResolutionScope is MResScopeType parentType) + { + if (IsTypeAccessAllowed(sandboxConfig, (MTypeReferenced)parentType.Type, out TypeConfig? parentCfg) == false) + { + cfg = null; + return false; + } + + if (parentCfg.All) + { + // Enclosing type is namespace-whitelisted so we don't have to check anything else. + cfg = TypeConfig.DefaultAll; + return true; + } + + // Found enclosing type, checking if we are allowed to access this nested type. + // Also pass it up in case of multiple nested types. + if (parentCfg.NestedTypes != null && parentCfg.NestedTypes.TryGetValue(type.Name, out cfg)) + { + return true; + } + + cfg = null; + return false; + } + + if (type.ResolutionScope is MResScopeAssembly mResScopeAssembly && + sandboxConfig.MultiAssemblyOtherReferences.Contains(mResScopeAssembly.Name)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + + // Types without namespaces or nesting parent are not allowed at all. + cfg = null; + return false; + } + + // Check if in whitelisted namespaces. + foreach (string whNamespace in sandboxConfig.WhitelistedNamespaces) + { + if (type.Namespace.StartsWith(whNamespace)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + } + + if (type.ResolutionScope is MResScopeAssembly resScopeAssembly && + sandboxConfig.MultiAssemblyOtherReferences.Contains(resScopeAssembly.Name)) + { + cfg = TypeConfig.DefaultAll; + return true; + } + + + if (sandboxConfig.Types.TryGetValue(type.Namespace, out Dictionary? nsDict) == false) + { + cfg = null; + return false; + } + + return nsDict.TryGetValue(type.Name, out cfg); + } + + private List<(MType type, MType parent, ArraySegment interfaceImpls)> GetExternalInheritedTypes( + MetadataReader reader, + ConcurrentBag errors) + { + List<(MType, MType, ArraySegment)> list = new List<(MType, MType, ArraySegment)>(); + foreach (TypeDefinitionHandle typeDefHandle in reader.TypeDefinitions) + { + TypeDefinition typeDef = reader.GetTypeDefinition(typeDefHandle); + ArraySegment interfaceImpls; + MTypeDefined type = AssemblyTypeCheckerHelpers.GetTypeFromDefinition(reader, typeDefHandle); + + if (!AssemblyTypeCheckerHelpers.ParseInheritType(type, typeDef.BaseType, out MType? parent, reader, errors)) + { + continue; + } + + InterfaceImplementationHandleCollection interfaceImplsCollection = typeDef.GetInterfaceImplementations(); + if (interfaceImplsCollection.Count == 0) + { + interfaceImpls = Array.Empty(); + } + else + { + interfaceImpls = new MType[interfaceImplsCollection.Count]; + int i = 0; + foreach (InterfaceImplementationHandle implHandle in interfaceImplsCollection) + { + InterfaceImplementation interfaceImpl = reader.GetInterfaceImplementation(implHandle); + + if (AssemblyTypeCheckerHelpers.ParseInheritType(type, interfaceImpl.Interface, out MType? implemented, reader, errors)) + { + interfaceImpls[i++] = implemented; + } + } + + interfaceImpls = interfaceImpls[..i]; + } + + list.Add((type, parent, interfaceImpls)); + } + + return list; + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Services/CodeScanConfigService.cs b/UnitystationLauncher/Services/CodeScanConfigService.cs new file mode 100644 index 00000000..190168af --- /dev/null +++ b/UnitystationLauncher/Services/CodeScanConfigService.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Pidgin; +using Serilog; +using UnitystationLauncher.ContentScanning; +using UnitystationLauncher.Models.ConfigFile; +using UnitystationLauncher.Models.ContentScanning; +using UnitystationLauncher.Models.Enums; +using UnitystationLauncher.Services.Interface; + +namespace UnitystationLauncher.Services; + +public class CodeScanConfigService : ICodeScanConfigService +{ + private static string NameConfig = @"CodeScanList.json"; + + private readonly HttpClient _httpClient; + + private readonly IPreferencesService _preferencesService; + private readonly IEnvironmentService _environmentService; + + private const string GoodFileURL = "Https://unitystationfile.b-cdn.net/GoodFiles/"; + + public CodeScanConfigService(HttpClient httpClient, IPreferencesService preferencesService, IEnvironmentService environmentService) + { + _httpClient = httpClient; + _preferencesService = preferencesService; + _environmentService = environmentService; + + } + + public async Task<(string, bool)> GetGoodFileVersion(string version) + { + if (await ValidGoodFilesVersion(version) == false) + { + return ("", false); + } + + string pathBase = _preferencesService.GetPreferences().InstallationPath; + string folderName = GetFolderName(version); + string versionPath = Path.Combine(pathBase, version, folderName); + + if (Directory.Exists(versionPath) == false) + { + string ZIPExtractPath = Path.Combine(pathBase, version); + HttpResponseMessage request = await _httpClient.GetAsync(GoodFileURL + version + "/" + folderName + ".zip", HttpCompletionOption.ResponseHeadersRead); + await using Stream responseStream = await request.Content.ReadAsStreamAsync(); + ZipArchive archive = new(responseStream); + archive.ExtractToDirectory(ZIPExtractPath, true); + + string ZIPDirectory = Path.Combine(ZIPExtractPath, GetZipFolderName()); + Directory.Move(ZIPDirectory, versionPath); + } + + return (versionPath, true); + } + + + private string GetZipFolderName() + { + CurrentEnvironment OS = _environmentService.GetCurrentEnvironment(); + switch (OS) + { + case CurrentEnvironment.WindowsStandalone: + return "Windows"; + case CurrentEnvironment.LinuxFlatpak: + case CurrentEnvironment.LinuxStandalone: + return "Linux"; + case CurrentEnvironment.MacOsStandalone: + return "Mac"; + default: + throw new Exception($"Unable to determine OS Version {OS}"); + } + } + + private string GetFolderName(string version) + { + CurrentEnvironment OS = _environmentService.GetCurrentEnvironment(); + switch (OS) + { + case CurrentEnvironment.WindowsStandalone: + return version + "_Windows"; + case CurrentEnvironment.LinuxFlatpak: + case CurrentEnvironment.LinuxStandalone: + return version + "_Linux"; + case CurrentEnvironment.MacOsStandalone: + return version + "_Mac"; + default: + throw new Exception($"Unable to determine OS Version {OS}"); + } + } + + public async Task ValidGoodFilesVersion(string goodFileVersion) + { + string jsonData = ""; + try + { + HttpResponseMessage response = await _httpClient.GetAsync("https://unitystationfile.b-cdn.net/GoodFiles/AllowGoodFiles.json"); + if (!response.IsSuccessStatusCode) + { + Log.Error("Unable to download config" + response); + return false; + } + + jsonData = await response.Content.ReadAsStringAsync(); + } + catch (Exception e) + { + Log.Error("Unable to download ValidGoodFilesVersion config" + e); + return false; + } + + + HashSet? allowedList = JsonSerializer.Deserialize>(jsonData, options: new() + { + IgnoreReadOnlyProperties = true, + PropertyNameCaseInsensitive = true + }); + + if (allowedList == null) + { + return false; + } + + return allowedList.Contains(goodFileVersion); + } + + public string SanitiseStringPath(string inString) + { + return inString.Replace(@"\", "").Replace("/", "").Replace(".", "_"); + } + + private static bool TryDownloadVersion() + { + return false; + } + + public async Task LoadConfigAsync() + { + string configPath = Path.Combine(_environmentService.GetUserdataDirectory(), NameConfig); + try + { + HttpResponseMessage response = await _httpClient.GetAsync("https://raw.githubusercontent.com/unitystation/unitystation/develop/CodeScanList.json"); + if (response.IsSuccessStatusCode) + { + string jsonData = await response.Content.ReadAsStringAsync(); + File.Delete(configPath); + await File.WriteAllTextAsync(configPath, jsonData); + Console.WriteLine("JSON file saved successfully."); + } + else + { + Log.Error("Unable to download config" + response.ToString()); + } + } + catch (Exception e) + { + Log.Error("Unable to download config" + e.ToString()); + } + + + if (Exists(configPath) == false) + { + Assembly assembly = Assembly.GetExecutingAssembly(); + string resourceName = "UnitystationLauncher.CodeScanList.json"; + using (Stream? stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream != null) + { + // Copy the contents of the resource to a file location + using (FileStream fileStream = File.Create(configPath)) + { + stream.Seek(0L, SeekOrigin.Begin); + await stream.CopyToAsync(fileStream); + } + } + } + Log.Error("had to use backup config"); + } + + using (StreamReader file = OpenText(configPath)) + { + try + { + SandboxConfig? data = JsonSerializer.Deserialize(file.ReadToEnd(), new JsonSerializerOptions + { + AllowTrailingCommas = true, + Converters = + { + new JsonStringEnumConverter(allowIntegerValues: false) + } + }); + + if (data == null) + { + Log.Error("unable to de-serialise config"); + throw new DataException("unable to de-serialise config"); + } + + foreach (KeyValuePair> @namespace in data.Types) + { + foreach (KeyValuePair @class in @namespace.Value) + { + ParseTypeConfig(@class.Value); + } + } + + return data; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + } + + private static void ParseTypeConfig(TypeConfig cfg) + { + if (cfg.Methods != null) + { + List list = new List(); + foreach (string m in cfg.Methods) + { + try + { + list.Add(Parsers.MethodParser.ParseOrThrow(m)); + } + catch (ParseException e) + { + Log.Error($"Parse exception for '{m}': {e}"); + } + } + + cfg.MethodsParsed = list.ToArray(); + } + else + { + cfg.MethodsParsed = Array.Empty(); + } + + if (cfg.Fields != null) + { + List list = new List(); + foreach (string f in cfg.Fields) + { + try + { + list.Add(Parsers.FieldParser.ParseOrThrow(f)); + } + catch (ParseException e) + { + Log.Error($"Parse exception for '{f}': {e}"); + throw; + } + } + + cfg.FieldsParsed = list.ToArray(); + } + else + { + cfg.FieldsParsed = Array.Empty(); + } + + if (cfg.NestedTypes != null) + { + foreach (TypeConfig nested in cfg.NestedTypes.Values) + { + ParseTypeConfig(nested); + } + } + } + + public StreamReader OpenText(string path) + { + return File.OpenText(path); + } + + public bool Exists(string path) + { + return File.Exists(path); + } +} \ No newline at end of file diff --git a/UnitystationLauncher/Services/CodeScanService.cs b/UnitystationLauncher/Services/CodeScanService.cs index d41477d5..51370ef8 100644 --- a/UnitystationLauncher/Services/CodeScanService.cs +++ b/UnitystationLauncher/Services/CodeScanService.cs @@ -3,7 +3,6 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Security.AccessControl; using System.Threading.Tasks; using Serilog; using UnitystationLauncher.Models.Enums; @@ -13,20 +12,16 @@ namespace UnitystationLauncher.Services; public class CodeScanService : ICodeScanService { - private readonly IAssemblyChecker _IAssemblyChecker; + private readonly IAssemblyTypeCheckerService _IAssemblyChecker; private readonly IEnvironmentService _environmentService; - private readonly IGoodFileService _iGoodFileService; + private readonly ICodeScanConfigService _iGoodFileService; private readonly IPreferencesService _preferencesService; private const string Managed = "Managed"; private const string Plugins = "Plugins"; private const string Unitystation_Data = "Unitystation_Data"; - - - - - public CodeScanService(IAssemblyChecker assemblyChecker, IEnvironmentService environmentService, IGoodFileService iGoodFileService, + public CodeScanService(IAssemblyTypeCheckerService assemblyChecker, IEnvironmentService environmentService, ICodeScanConfigService iGoodFileService, IPreferencesService ipreferencesService) { _IAssemblyChecker = assemblyChecker; @@ -37,24 +32,12 @@ public CodeScanService(IAssemblyChecker assemblyChecker, IEnvironmentService env - class FileInfoComparer : IEqualityComparer - { - public bool Equals(FileInfo? x, FileInfo? y) - { - if (x == null || y == null) return false; - return x.Name.Equals(y.Name, StringComparison.OrdinalIgnoreCase); - } - public int GetHashCode(FileInfo obj) - { - return obj.Name.GetHashCode(); - } - } public async Task OnScan(ZipArchive archive, string targetDirectory, string goodFileVersion, Action info, Action errors) { // TODO: Enable extraction cancelling - var root = new DirectoryInfo(_preferencesService.GetPreferences().InstallationPath); + DirectoryInfo root = new DirectoryInfo(_preferencesService.GetPreferences().InstallationPath); DirectoryInfo stagingDirectory = root.CreateSubdirectory("UnsafeBuildZipDirectory"); DirectoryInfo processingDirectory = root.CreateSubdirectory("UnsafeBuildProcessing"); @@ -84,9 +67,9 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin if (_environmentService.GetCurrentEnvironment() != CurrentEnvironment.MacOsStandalone) { // Get all files in the directory - var directories = processingDirectory.GetDirectories(); + DirectoryInfo[] directories = processingDirectory.GetDirectories(); // Loop through each file - foreach (var directorie in directories) + foreach (DirectoryInfo directorie in directories) { if (directorie.Name.Contains("_Data")) { @@ -116,11 +99,11 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin - var dllDirectory = dataPath.CreateSubdirectory(Managed); + DirectoryInfo dllDirectory = dataPath.CreateSubdirectory(Managed); CopyFilesRecursively(stagingManaged.ToString(), dllDirectory.ToString()); - var (goodFilePath, booly) = await _iGoodFileService.GetGoodFileVersion(goodFileVersion); + (string goodFilePath, bool booly) = await _iGoodFileService.GetGoodFileVersion(goodFileVersion); if (booly == false) { @@ -151,7 +134,7 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin CopyFilesRecursively(goodFilePath, processingDirectory.ToString()); if (dataPath.Name != Unitystation_Data && _environmentService.GetCurrentEnvironment() != CurrentEnvironment.MacOsStandalone) //I know Cases and to file systems but F { - var oldPath = Path.Combine(processingDirectory.ToString(), Unitystation_Data); + string oldPath = Path.Combine(processingDirectory.ToString(), Unitystation_Data); CopyFilesRecursively(oldPath, dataPath.ToString()); Directory.Delete(oldPath, true); } @@ -160,7 +143,7 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin switch (_environmentService.GetCurrentEnvironment()) { case CurrentEnvironment.WindowsStandalone: - var exeRename = processingDirectory.GetFiles() + FileInfo? exeRename = processingDirectory.GetFiles() .FirstOrDefault(x => x.Extension == ".exe" && x.Name != "UnityCrashHandler64.exe"); //TODO OS @@ -176,7 +159,7 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin break; case CurrentEnvironment.LinuxFlatpak: case CurrentEnvironment.LinuxStandalone: - var ExecutableRename = processingDirectory.GetFiles() + FileInfo? ExecutableRename = processingDirectory.GetFiles() .FirstOrDefault(x => x.Extension == ""); if (ExecutableRename == null || ExecutableRename.Directory == null) @@ -192,7 +175,7 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin } - var targetDirectoryinfo = new DirectoryInfo(targetDirectory); + DirectoryInfo targetDirectoryinfo = new DirectoryInfo(targetDirectory); if (targetDirectoryinfo.Exists) { DeleteContentsOfDirectory(targetDirectoryinfo); @@ -215,7 +198,7 @@ public async Task OnScan(ZipArchive archive, string targetDirectory, strin public string GetManagedOnOS(string GoodFiles) { - var OS = _environmentService.GetCurrentEnvironment(); + CurrentEnvironment OS = _environmentService.GetCurrentEnvironment(); switch (OS) { case CurrentEnvironment.WindowsStandalone: @@ -234,25 +217,25 @@ public string GetManagedOnOS(string GoodFiles) public bool ScanFolder(DirectoryInfo @unsafe, DirectoryInfo saveFiles, Action info, Action errors) { - var goodFiles = saveFiles.GetFiles().Select(x => x.Name).ToList(); + List goodFiles = saveFiles.GetFiles().Select(x => x.Name).ToList(); info.Invoke("Provided files " + string.Join(",", goodFiles)); CopyFilesRecursively(saveFiles.ToString(), @unsafe.ToString()); - var files = @unsafe.GetFiles(); + FileInfo[] files = @unsafe.GetFiles(); List multiAssemblyReference = new List(); - foreach (var file in files) + foreach (FileInfo file in files) { if (goodFiles.Contains(file.Name)) continue; multiAssemblyReference.Add(Path.GetFileNameWithoutExtension(file.Name)); } - foreach (var file in files) + foreach (FileInfo file in files) { if (goodFiles.Contains(file.Name)) continue; @@ -260,7 +243,7 @@ public bool ScanFolder(DirectoryInfo @unsafe, DirectoryInfo saveFiles, Action listy = multiAssemblyReference.ToList(); listy.Remove(Path.GetFileNameWithoutExtension(file.Name)); if (_IAssemblyChecker.CheckAssembly(file, @unsafe, listy, info, errors) == false) { @@ -285,15 +268,15 @@ public bool ScanFolder(DirectoryInfo @unsafe, DirectoryInfo saveFiles, Action GetGoodFileVersion(string version) - { - if (await ValidGoodFilesVersion(version) == false) - { - return ("", false); - } - - var pathBase = _preferencesService.GetPreferences().InstallationPath; - var folderName = GetFolderName(version); - var versionPath = Path.Combine(pathBase, version, folderName); - - if (Directory.Exists(versionPath) == false) - { - var ZIPExtractPath = Path.Combine(pathBase, version); - HttpResponseMessage request = await _httpClient.GetAsync(GoodFileURL + version + "/" + folderName + ".zip", HttpCompletionOption.ResponseHeadersRead); - await using Stream responseStream = await request.Content.ReadAsStreamAsync(); - ZipArchive archive = new(responseStream); - archive.ExtractToDirectory(ZIPExtractPath, true); - - var ZIPDirectory = Path.Combine(ZIPExtractPath, GetZipFolderName()); - Directory.Move(ZIPDirectory, versionPath); - } - - return (versionPath, true); - } - - - private string GetZipFolderName() - { - var OS = _environmentService.GetCurrentEnvironment(); - switch (OS) - { - case CurrentEnvironment.WindowsStandalone: - return "Windows"; - case CurrentEnvironment.LinuxFlatpak: - case CurrentEnvironment.LinuxStandalone: - return "Linux"; - case CurrentEnvironment.MacOsStandalone: - return "Mac"; - default: - throw new Exception($"Unable to determine OS Version {OS}"); - } - } - - private string GetFolderName(string version) - { - var OS = _environmentService.GetCurrentEnvironment(); - switch (OS) - { - case CurrentEnvironment.WindowsStandalone: - return version + "_Windows"; - case CurrentEnvironment.LinuxFlatpak: - case CurrentEnvironment.LinuxStandalone: - return version + "_Linux"; - case CurrentEnvironment.MacOsStandalone: - return version + "_Mac"; - default: - throw new Exception($"Unable to determine OS Version {OS}"); - } - } - - public async Task ValidGoodFilesVersion(string goodFileVersion) - { - var jsonData = ""; - try - { - var response = await _httpClient.GetAsync("https://unitystationfile.b-cdn.net/GoodFiles/AllowGoodFiles.json"); - if (!response.IsSuccessStatusCode) - { - Log.Error("Unable to download config" + response); - return false; - } - - jsonData = await response.Content.ReadAsStringAsync(); - } - catch (Exception e) - { - Log.Error("Unable to download ValidGoodFilesVersion config" + e); - return false; - } - - - var allowedList = JsonSerializer.Deserialize>(jsonData, options: new() - { - IgnoreReadOnlyProperties = true, - PropertyNameCaseInsensitive = true - }); - - if (allowedList == null) - { - return false; - } - - return allowedList.Contains(goodFileVersion); - } - - public string SanitiseStringPath(string inString) - { - return inString.Replace(@"\", "").Replace("/", "").Replace(".", "_"); - } - - private bool TryDownloadVersion() - { - return false; - } -} \ No newline at end of file diff --git a/UnitystationLauncher/Services/HubBuildCommunicationPipeService.cs b/UnitystationLauncher/Services/HubBuildCommunicationPipeService.cs deleted file mode 100644 index 47cedcf1..00000000 --- a/UnitystationLauncher/Services/HubBuildCommunicationPipeService.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.IO; -using System.IO.Pipes; -using System.Reactive.Concurrency; -using System.Threading.Tasks; -using MessageBox.Avalonia.BaseWindows.Base; -using ReactiveUI; -using Serilog; -using UnitystationLauncher.Infrastructure; -using UnitystationLauncher.Models.Enums; -using UnitystationLauncher.Services.Interface; - -namespace UnitystationLauncher.Services; - -public static class HubBuildCommunicationPipeService -{ - - private static PipeHubBuildCommunication? _coolPipeHubBuildCommunication; - - public static void Init() - { - var data = new PipeHubBuildCommunication(); - _ = data.StartServerPipe(); - _coolPipeHubBuildCommunication = data; - } - - - public class PipeHubBuildCommunication : IDisposable - { - private NamedPipeServerStream _serverPipe; - private StreamReader? _reader; - private StreamWriter? _writer; - - public PipeHubBuildCommunication() - { - _serverPipe = new NamedPipeServerStream("Unitystation_Hub_Build_Communication", PipeDirection.InOut, 1, - PipeTransmissionMode.Byte, PipeOptions.Asynchronous); - } - - private enum ClientRequest - { - URL = 1, - API_URL = 2, - Host_Trust_Mode = 3, - } - - public async Task StartServerPipe() - { - await _serverPipe.WaitForConnectionAsync(); - _reader = new StreamReader(_serverPipe); - _writer = new StreamWriter(_serverPipe); - - while (true) - { - string? request = await _reader.ReadLineAsync(); - if (request == null) - { - try - { - await _serverPipe.WaitForConnectionAsync(); - } - catch (System.IO.IOException e) - { - Log.Error(e.ToString()); - _serverPipe.Close(); - _serverPipe = new NamedPipeServerStream("Unitystation_Hub_Build_Communication", PipeDirection.InOut, - 1, - PipeTransmissionMode.Byte, PipeOptions.Asynchronous); - await _serverPipe.WaitForConnectionAsync(); - } - - _reader = new StreamReader(_serverPipe); - _writer = new StreamWriter(_serverPipe); - continue; - } - - var requests = request.Split(","); - Log.Information($"Server: Received request: {request}"); - - if (ClientRequest.URL.ToString() == requests[0]) - { - RxApp.MainThreadScheduler.ScheduleAsync(async (_, _) => - { - IMsBoxWindow msgBox = MessageBoxBuilder.CreateMessageBox( - MessageBoxButtons.YesNo, - string.Empty, - $"would you like to add this Domain to The allowed domains to be opened In your browser, {requests[1]} " + - @" - Justification given by the Fork : " + requests[2]); - - string response = await msgBox.Show(); - Log.Information($"response {response}"); - await _writer.WriteLineAsync(response == "No" ? false.ToString() : true.ToString()); - await _writer.FlushAsync(); - return Task.CompletedTask; - }); - } - else if (ClientRequest.API_URL.ToString() == requests[0]) - { - RxApp.MainThreadScheduler.ScheduleAsync(async (_, _) => - { - IMsBoxWindow msgBox = MessageBoxBuilder.CreateMessageBox( - MessageBoxButtons.YesNo, - string.Empty, - $"The build would like to send an API request to, {requests[1]} " + @" - do you allow this fork to now on access this domain - Justification given by the Fork : " + requests[2]); - - - string response = await msgBox.Show(); - Log.Information($"response {response}"); - await _writer.WriteLineAsync(response == "No" ? false.ToString() : true.ToString()); - await _writer.FlushAsync(); - return Task.CompletedTask; - }); - } - else if (ClientRequest.Host_Trust_Mode.ToString() == requests[0]) - { - RxApp.MainThreadScheduler.ScheduleAsync(async (_, _) => - { - IMsBoxWindow msgBox = MessageBoxBuilder.CreateMessageBox( - MessageBoxButtons.YesNo, - string.Empty, - @" Trusted mode automatically allows every API and open URL action to happen without prompt, this also enables the - Variable viewer ( Application that can modify the games Data ) that Could potentially be used to Perform malicious actions on your PC, - The main purpose of this Prompt is to allow the Variable viewer (Variable editing), - What follows is given by the build, we do not control what is written in the Following text So treat with caution and use your brain - Justification : " + requests[1]); //TODO Add text - - string response = await msgBox.Show(); - Log.Information($"response {response}"); - await _writer.WriteLineAsync(response == "No" ? false.ToString() : true.ToString()); - await _writer.FlushAsync(); - return Task.CompletedTask; - }); - } - } - } - - public void Dispose() - { - _serverPipe.Dispose(); - _reader?.Dispose(); - _writer?.Dispose(); - } - } -} \ No newline at end of file diff --git a/UnitystationLauncher/Services/InstallationService.cs b/UnitystationLauncher/Services/InstallationService.cs index 8424cbf3..1895ddd9 100644 --- a/UnitystationLauncher/Services/InstallationService.cs +++ b/UnitystationLauncher/Services/InstallationService.cs @@ -31,7 +31,7 @@ public class InstallationService : IInstallationService private readonly IServerService _serverService; private readonly ICodeScanService _codeScanService; - private readonly IGoodFileService _iGoodFileService; + private readonly ICodeScanConfigService _iGoodFileService; private readonly List _downloads; private List _installations = new(); @@ -39,7 +39,7 @@ public class InstallationService : IInstallationService public InstallationService(HttpClient httpClient, IPreferencesService preferencesService, IEnvironmentService environmentService, IServerService serverService, ICodeScanService codeScanService, - IGoodFileService iGoodFileService) + ICodeScanConfigService iGoodFileService) { _httpClient = httpClient; _preferencesService = preferencesService; @@ -90,7 +90,7 @@ public List GetInstallations() server.ServerGoodFileVersion = "1.0.0"; //TODO - var result = await _iGoodFileService.ValidGoodFilesVersion(server.ServerGoodFileVersion); + bool result = await _iGoodFileService.ValidGoodFilesVersion(server.ServerGoodFileVersion); if (result == false) { @@ -114,7 +114,7 @@ public List GetInstallations() download = new(downloadUrl, installationPath, server.ForkName, server.BuildVersion, server.ServerGoodFileVersion); - (bool canStartDownload, string cantDownloadReason) = CanStartDownload(download); + (bool canStartDownload, string cantDownloadReason) = InstallationService.CanStartDownload(download); if (!canStartDownload) { @@ -157,7 +157,7 @@ public List GetInstallations() EnsureExecutableFlagOnUnixSystems(executable); - string arguments = GetArguments(server, port); + string arguments = InstallationService.GetArguments(server, port); ProcessStartInfo? startInfo = _environmentService.GetGameProcessStartInfo(executable, arguments); if (startInfo == null) @@ -269,7 +269,7 @@ public bool MoveInstallations(string newBasePath) continue; } - CreateParentDirectory(newPath); + InstallationService.CreateParentDirectory(newPath); if (Directory.Exists(newPath)) { @@ -367,7 +367,7 @@ private void WriteInstallations() Log.Debug("Installations JSON written"); } - private void CreateParentDirectory(string path) + private static void CreateParentDirectory(string path) { DirectoryInfo directoryInfo = new(path); string? parentDir = directoryInfo.Parent?.FullName; @@ -383,7 +383,7 @@ private void CreateParentDirectory(string path) return _installations.FirstOrDefault(i => i.InstallationId == installationId); } - private (bool, string) CanStartDownload(Download download) + private static (bool, string) CanStartDownload(Download download) { if (Directory.Exists(download.InstallPath)) { @@ -402,7 +402,7 @@ private void CreateParentDirectory(string path) return (true, string.Empty); } - private string GetArguments(string? server, long? port) + private static string GetArguments(string? server, long? port) { string arguments = string.Empty; @@ -434,7 +434,7 @@ private async Task StartDownloadAsync(Download download) download.Size = request.Content.Headers.ContentLength ?? throw new ContentLengthNullException(download.DownloadUrl); - using IDisposable logProgressDisposable = LogProgress(progressStream, download); + using IDisposable logProgressDisposable = InstallationService.LogProgress(progressStream, download); using IDisposable progressDisposable = progressStream.Progress .Subscribe(p => { download.Downloaded = p; }); @@ -458,7 +458,7 @@ await Task.Run(() => Console.WriteLine($"error {log}"); ErrorList.Add(log); }); - var scanTask = _codeScanService.OnScan(archive, download.InstallPath, download.GoodFileVersion, + Task scanTask = _codeScanService.OnScan(archive, download.InstallPath, download.GoodFileVersion, info, errors); scanTask.Wait(); if (scanTask.Result) @@ -506,7 +506,7 @@ await Task.Run(() => } } - private IDisposable LogProgress(ProgressStream progressStream, Download download) + private static IDisposable LogProgress(ProgressStream progressStream, Download download) { long lastPosition = 0L; DateTime lastTime = DateTime.Now; diff --git a/UnitystationLauncher/Services/Interface/IAssemblyChecker.cs b/UnitystationLauncher/Services/Interface/IAssemblyTypeCheckerService.cs similarity index 85% rename from UnitystationLauncher/Services/Interface/IAssemblyChecker.cs rename to UnitystationLauncher/Services/Interface/IAssemblyTypeCheckerService.cs index 72ac5074..51536a44 100644 --- a/UnitystationLauncher/Services/Interface/IAssemblyChecker.cs +++ b/UnitystationLauncher/Services/Interface/IAssemblyTypeCheckerService.cs @@ -4,7 +4,7 @@ namespace UnitystationLauncher.Services.Interface; -public interface IAssemblyChecker +public interface IAssemblyTypeCheckerService { public bool CheckAssembly(FileInfo diskPath, DirectoryInfo managedPath, List otherAssemblies, Action info, Action errors); } \ No newline at end of file diff --git a/UnitystationLauncher/Services/Interface/IGoodFileService.cs b/UnitystationLauncher/Services/Interface/ICodeScanConfigService.cs similarity index 67% rename from UnitystationLauncher/Services/Interface/IGoodFileService.cs rename to UnitystationLauncher/Services/Interface/ICodeScanConfigService.cs index aafe8f85..7e4f24be 100644 --- a/UnitystationLauncher/Services/Interface/IGoodFileService.cs +++ b/UnitystationLauncher/Services/Interface/ICodeScanConfigService.cs @@ -1,12 +1,15 @@ using System.Threading.Tasks; +using UnitystationLauncher.Models.ConfigFile; namespace UnitystationLauncher.Services.Interface; -public interface IGoodFileService +public interface ICodeScanConfigService { public Task<(string, bool)> GetGoodFileVersion(string version); public Task ValidGoodFilesVersion(string goodFileVersion); public string SanitiseStringPath(string inString); + + public Task LoadConfigAsync(); } \ No newline at end of file diff --git a/UnitystationLauncher/Services/Interface/IFileService.cs b/UnitystationLauncher/Services/Interface/IFileService.cs deleted file mode 100644 index c22c4aec..00000000 --- a/UnitystationLauncher/Services/Interface/IFileService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.IO; - -namespace UnitystationLauncher.Services.Interface; - -public interface IFileService -{ - public StreamReader OpenText(string path); - - public bool Exists(string path); -} \ No newline at end of file diff --git a/UnitystationLauncher/Services/Interface/IGameCommunicationPipeService.cs b/UnitystationLauncher/Services/Interface/IGameCommunicationPipeService.cs new file mode 100644 index 00000000..4f45e365 --- /dev/null +++ b/UnitystationLauncher/Services/Interface/IGameCommunicationPipeService.cs @@ -0,0 +1,4 @@ +public interface IGameCommunicationPipeService +{ + public void Init(); +} \ No newline at end of file diff --git a/UnitystationLauncher/StandardModule.cs b/UnitystationLauncher/StandardModule.cs index 3d4f89ee..0d311d60 100644 --- a/UnitystationLauncher/StandardModule.cs +++ b/UnitystationLauncher/StandardModule.cs @@ -1,6 +1,5 @@ using System.Net.Http; using Autofac; -using UnitystationLauncher.ContentScanning; using UnitystationLauncher.Services; using UnitystationLauncher.Services.Interface; @@ -19,13 +18,12 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); - HubBuildCommunicationPipeService.Init(); // View Models builder.RegisterAssemblyTypes(ThisAssembly) .Where(t => t.Name.EndsWith("ViewModel")); diff --git a/UnitystationLauncher/ViewModels/LauncherViewModel.cs b/UnitystationLauncher/ViewModels/LauncherViewModel.cs index f2d749be..bb0d9b52 100644 --- a/UnitystationLauncher/ViewModels/LauncherViewModel.cs +++ b/UnitystationLauncher/ViewModels/LauncherViewModel.cs @@ -48,25 +48,27 @@ public LauncherViewModel( PreferencesPanelViewModel preferencesPanel, IHubService hubService, IPreferencesService preferencesService, - IEnvironmentService environmentService) + IEnvironmentService environmentService, + IGameCommunicationPipeService gameCommunicationPipeService) { _hubUpdateVm = hubUpdateVm; _hubService = hubService; _preferencesService = preferencesService; _environmentService = environmentService; + gameCommunicationPipeService.Init(); OpenMainSite = ReactiveCommand.Create(() => OpenLink(LinkUrls.MainSiteUrl)); OpenPatreon = ReactiveCommand.Create(() => OpenLink(LinkUrls.PatreonUrl)); OpenDiscordInvite = ReactiveCommand.Create(() => OpenLink(LinkUrls.DiscordInviteUrl)); - _panels = GetEnabledPanels(newsPanel, serversPanel, installationsPanel, preferencesPanel); + _panels = LauncherViewModel.GetEnabledPanels(newsPanel, serversPanel, installationsPanel, preferencesPanel); ShowUpdateView = ReactiveCommand.Create(ShowUpdateImp); SelectedPanel = serversPanel; RxApp.MainThreadScheduler.ScheduleAsync((_, _) => ValidateClientVersionAsync()); } - private PanelBase[] GetEnabledPanels( + private static PanelBase[] GetEnabledPanels( NewsPanelViewModel newsPanel, ServersPanelViewModel serversPanel, InstallationsPanelViewModel installationsPanel,