diff --git a/NuGet.Config b/NuGet.Config index 051f66f..e50bcc2 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,6 +3,5 @@ - \ No newline at end of file diff --git a/source/DirectoryServices.Tests/ActiveDirectoryMembershipTests.cs b/source/DirectoryServices.Tests/ActiveDirectoryMembershipTests.cs index ac41be5..71ce338 100644 --- a/source/DirectoryServices.Tests/ActiveDirectoryMembershipTests.cs +++ b/source/DirectoryServices.Tests/ActiveDirectoryMembershipTests.cs @@ -14,7 +14,7 @@ public class ActiveDirectoryMembershipTests [TestCase("EXAMPLE\\joe", "joe", "EXAMPLE")] public void CredentialsAreNormalized(string rawUsername, string usedUsername, string usedDomain) { - var log = Substitute.For(); + var log = Substitute.For(); var credentialNormalizer = new DirectoryServicesObjectNameNormalizer(log); var domainUser = credentialNormalizer.NormalizeName(rawUsername); Assert.AreEqual(usedUsername, domainUser.NormalizedName); diff --git a/source/DirectoryServices.Tests/CredentialValidatorTests.cs b/source/DirectoryServices.Tests/CredentialValidatorTests.cs index 976461d..df9bf6f 100644 --- a/source/DirectoryServices.Tests/CredentialValidatorTests.cs +++ b/source/DirectoryServices.Tests/CredentialValidatorTests.cs @@ -38,7 +38,7 @@ public void SetUp() directoryServicesService = Substitute.For(); - var log = Substitute.For(); + var log = Substitute.For(); identityCreator = new IdentityCreator(); diff --git a/source/DirectoryServices.Tests/DirectoryServicesExternalSecurityGroupLocatorTests.cs b/source/DirectoryServices.Tests/DirectoryServicesExternalSecurityGroupLocatorTests.cs index 6120308..9055c9b 100644 --- a/source/DirectoryServices.Tests/DirectoryServicesExternalSecurityGroupLocatorTests.cs +++ b/source/DirectoryServices.Tests/DirectoryServicesExternalSecurityGroupLocatorTests.cs @@ -18,12 +18,12 @@ public class DirectoryServicesExternalSecurityGroupLocatorTests DirectoryServicesExternalSecurityGroupLocator locator; IUserPrincipalFinder userPrincipalFinder; - InMemoryLog log; + ISystemLog log; [SetUp] public void SetUp() { - log = new InMemoryLog(); + log = Substitute.For(); var configurationStore = Substitute.For(); var contextProvider = Substitute.For(); @@ -33,7 +33,7 @@ public void SetUp() var directoryServicesObjectNameNormalizer = Substitute.For(); directoryServicesObjectNameNormalizer.NormalizeName(Arg.Any()) .Returns(x => new DomainUser(null, ((string)x.Args()[0]))); - + locator = new DirectoryServicesExternalSecurityGroupLocator( log, contextProvider, @@ -41,40 +41,42 @@ public void SetUp() configurationStore, userPrincipalFinder ); - + configurationStore.GetAreSecurityGroupsEnabled().Returns(true); contextProvider.GetContext(null).ReturnsForAnyArgs(new PrincipalContext(ContextType.Machine)); - } - + [Test] public void GetGroupIdsForUser_NotFound() { userPrincipalFinder.FindByIdentity(null, null).ReturnsForAnyArgs((IUserPrincipalWrapper) null); - + var result = locator.GetGroupIdsForUser("Bob", CancellationToken.None); result.WasAbleToRetrieveGroups.ShouldBeFalse(); - - log.Logs.ShouldContain((LogCategory.Trace, null, $"While loading security groups, a principal identifiable by 'Bob' was not found in '{Environment.MachineName}'")); + + log.Received().Trace($"While loading security groups, a principal identifiable by 'Bob' was not found in '{Environment.MachineName}'"); } - + [Test] public void GetGroupIdsForUser_ErrorGettingAuthorizationGroups() { - + var userPrincipal = Substitute.For(); userPrincipalFinder.FindByIdentity(null, null).ReturnsForAnyArgs(userPrincipal); var authGroupsException = new Exception("AuthorizationGroups Exception"); userPrincipal.GetAuthorizationGroups(CancellationToken.None).ThrowsForAnyArgs(authGroupsException); userPrincipal.GetGroups(CancellationToken.None).Returns(new[] {new FakeGroupPrincipal(groupSid)}); - + var result = locator.GetGroupIdsForUser("Bob", CancellationToken.None); result.WasAbleToRetrieveGroups.ShouldBeTrue(); result.GroupsIds.ShouldBe(new[] { groupSid}); - - log.Logs.ShouldContain(l => l.category == LogCategory.Verbose && l.exception == authGroupsException); - log.Logs.ShouldNotContain(l => l.category == LogCategory.Error || l.category == LogCategory.Fatal); + + log.Received().Verbose(authGroupsException); + log.DidNotReceive().Error(Arg.Any()); + log.DidNotReceive().Error(Arg.Any()); + log.DidNotReceive().Fatal(Arg.Any()); + log.DidNotReceive().Fatal(Arg.Any()); } [Test] @@ -85,34 +87,37 @@ public void GetGroupIdsForUser_ErrorGettingAnyGroups() var authGroupsException = new Exception("AuthorizationGroups Exception"); userPrincipal.GetAuthorizationGroups(CancellationToken.None).ThrowsForAnyArgs(authGroupsException); - + var groupsException = new Exception("Groups Exception"); userPrincipal.GetGroups(CancellationToken.None).ThrowsForAnyArgs(groupsException); - + var result = locator.GetGroupIdsForUser("Bob", CancellationToken.None); - + result.WasAbleToRetrieveGroups.ShouldBeFalse(); result.GroupsIds.ShouldBeNull(); - - log.Logs.ShouldContain(l => l.category == LogCategory.Verbose && l.exception == authGroupsException); - log.Logs.ShouldContain(l => l.category == LogCategory.Error && l.exception == groupsException); + + log.Received().Verbose(authGroupsException); + log.Received().ErrorFormat(groupsException, "Active Directory search for {0} failed.", "Bob"); } - + [Test] public void GetGroupIdsForUser_Success() { - + var userPrincipal = Substitute.For(); userPrincipalFinder.FindByIdentity(null, null).ReturnsForAnyArgs(userPrincipal); userPrincipal.GetAuthorizationGroups(CancellationToken.None).ReturnsForAnyArgs(new[] {new FakeGroupPrincipal(groupSid)}); - + var result = locator.GetGroupIdsForUser("Bob", CancellationToken.None); - + result.WasAbleToRetrieveGroups.ShouldBeTrue(); result.GroupsIds.ShouldBe(new[] { groupSid}); - log.Logs.ShouldNotContain(l => l.category == LogCategory.Error || l.category == LogCategory.Fatal); + log.DidNotReceive().Error(Arg.Any()); + log.DidNotReceive().Error(Arg.Any()); + log.DidNotReceive().Fatal(Arg.Any()); + log.DidNotReceive().Fatal(Arg.Any()); } class FakeGroupPrincipal : IPrincipalWrapper diff --git a/source/DirectoryServices.Tests/InMemoryLog.cs b/source/DirectoryServices.Tests/InMemoryLog.cs deleted file mode 100644 index 5594ea6..0000000 --- a/source/DirectoryServices.Tests/InMemoryLog.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using Octopus.Diagnostics; - -namespace DirectoryServices.Tests -{ - public class InMemoryLog : ILog - { - public List<(LogCategory category, Exception exception, string message)> Logs { get; }= new List<(LogCategory category, Exception exception, string message)>(); - - public void Trace(string messageText) => Write(LogCategory.Trace, messageText); - public void Trace(Exception error) => Write(LogCategory.Trace, error); - public void Trace(Exception error, string messageText) => Write(LogCategory.Trace, error, messageText); - public void TraceFormat(string messageFormat, params object[] args) => WriteFormat(LogCategory.Trace, messageFormat, args); - public void TraceFormat(Exception error, string format, params object[] args) => WriteFormat(LogCategory.Trace, error, format, args); - - public void Verbose(string messageText) => Write(LogCategory.Verbose, messageText); - public void Verbose(Exception error) => Write(LogCategory.Verbose, error); - public void Verbose(Exception error, string messageText) => Write(LogCategory.Verbose, error, messageText); - public void VerboseFormat(string messageFormat, params object[] args) => WriteFormat(LogCategory.Verbose, messageFormat, args); - public void VerboseFormat(Exception error, string format, params object[] args) => WriteFormat(LogCategory.Verbose, error, format, args); - - public void Info(string messageText) => Write(LogCategory.Info, messageText); - public void Info(Exception error) => Write(LogCategory.Info, error); - public void Info(Exception error, string messageText) => Write(LogCategory.Info, error, messageText); - public void InfoFormat(string messageFormat, params object[] args) => WriteFormat(LogCategory.Info, messageFormat, args); - public void InfoFormat(Exception error, string format, params object[] args) => WriteFormat(LogCategory.Info, error, format, args); - - public void Warn(string messageText) => Write(LogCategory.Warning, messageText); - public void Warn(Exception error) => Write(LogCategory.Warning, error); - public void Warn(Exception error, string messageText) => Write(LogCategory.Warning, error, messageText); - public void WarnFormat(string messageFormat, params object[] args) => WriteFormat(LogCategory.Warning, messageFormat, args); - public void WarnFormat(Exception error, string format, params object[] args) => WriteFormat(LogCategory.Warning, error, format, args); - - public void Error(string messageText) => Write(LogCategory.Error, messageText); - public void Error(Exception error) => Write(LogCategory.Error, error); - public void Error(Exception error, string messageText) => Write(LogCategory.Error, error, messageText); - public void ErrorFormat(string messageFormat, params object[] args) => WriteFormat(LogCategory.Error, messageFormat, args); - public void ErrorFormat(Exception error, string format, params object[] args) => WriteFormat(LogCategory.Error, error, format, args); - - public void Fatal(string messageText) => Write(LogCategory.Fatal, messageText); - public void Fatal(Exception error) => Write(LogCategory.Fatal, error); - public void Fatal(Exception error, string messageText) => Write(LogCategory.Fatal, error, messageText); - public void FatalFormat(string messageFormat, params object[] args) => WriteFormat(LogCategory.Fatal, messageFormat, args); - public void FatalFormat(Exception error, string format, params object[] args) => WriteFormat(LogCategory.Fatal, error, format, args); - - public void Write(LogCategory category, string messageText) => Write(category, null, messageText); - public void Write(LogCategory category, Exception error) => Write(category, error, null); - public void WriteFormat(LogCategory category, string messageFormat, params object[] args) => Write(category, null, string.Format(messageFormat, args)); - public void WriteFormat(LogCategory category, Exception error, string messageFormat, params object[] args) => Write(category, error, string.Format(messageFormat, args)); - - public void Write(LogCategory category, Exception error, string messageText) - { - lock (Logs) - Logs.Add((category, error, messageText)); - } - - public void Flush() - { - } - } -} \ No newline at end of file diff --git a/source/Server/Configuration/DatabaseInitializer.cs b/source/Server/Configuration/DatabaseInitializer.cs index 2c5cff4..231f56d 100644 --- a/source/Server/Configuration/DatabaseInitializer.cs +++ b/source/Server/Configuration/DatabaseInitializer.cs @@ -10,13 +10,13 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Configur { class DatabaseInitializer : ExecuteWhenDatabaseInitializes { - readonly ILog log; + readonly ISystemLog log; readonly IConfigurationStore configurationStore; readonly IWritableKeyValueStore settings; bool cleanupRequired = false; - public DatabaseInitializer(ILog log, IConfigurationStore configurationStore, IWritableKeyValueStore settings) + public DatabaseInitializer(ISystemLog log, IConfigurationStore configurationStore, IWritableKeyValueStore settings) { this.log = log; this.configurationStore = configurationStore; diff --git a/source/Server/Configuration/DirectoryServicesConfigureCommands.cs b/source/Server/Configuration/DirectoryServicesConfigureCommands.cs index 9985a52..2c54d2d 100644 --- a/source/Server/Configuration/DirectoryServicesConfigureCommands.cs +++ b/source/Server/Configuration/DirectoryServicesConfigureCommands.cs @@ -8,11 +8,11 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Configur { class DirectoryServicesConfigureCommands : IContributeToConfigureCommand { - readonly ILog log; + readonly ISystemLog log; readonly Lazy activeDirectoryConfiguration; public DirectoryServicesConfigureCommands( - ILog log, + ISystemLog log, Lazy activeDirectoryConfiguration) { this.log = log; diff --git a/source/Server/DirectoryServices/DirectoryServicesCredentialValidator.cs b/source/Server/DirectoryServices/DirectoryServicesCredentialValidator.cs index 402f3db..4845343 100644 --- a/source/Server/DirectoryServices/DirectoryServicesCredentialValidator.cs +++ b/source/Server/DirectoryServices/DirectoryServicesCredentialValidator.cs @@ -14,7 +14,7 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Director { class DirectoryServicesCredentialValidator : IDirectoryServicesCredentialValidator { - readonly ILog log; + readonly ISystemLog log; readonly IDirectoryServicesObjectNameNormalizer objectNameNormalizer; readonly IUpdateableUserStore userStore; readonly IDirectoryServicesConfigurationStore configurationStore; @@ -24,7 +24,7 @@ class DirectoryServicesCredentialValidator : IDirectoryServicesCredentialValidat internal static string EnvironmentUserDomainName = Environment.UserDomainName; public DirectoryServicesCredentialValidator( - ILog log, + ISystemLog log, IDirectoryServicesObjectNameNormalizer objectNameNormalizer, IUpdateableUserStore userStore, IDirectoryServicesConfigurationStore configurationStore, diff --git a/source/Server/DirectoryServices/DirectoryServicesExternalSecurityGroupLocator.cs b/source/Server/DirectoryServices/DirectoryServicesExternalSecurityGroupLocator.cs index e7a38d0..d9d572c 100644 --- a/source/Server/DirectoryServices/DirectoryServicesExternalSecurityGroupLocator.cs +++ b/source/Server/DirectoryServices/DirectoryServicesExternalSecurityGroupLocator.cs @@ -13,14 +13,14 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Director { class DirectoryServicesExternalSecurityGroupLocator : IDirectoryServicesExternalSecurityGroupLocator { - readonly ILog log; + readonly ISystemLog log; readonly IDirectoryServicesContextProvider contextProvider; readonly IDirectoryServicesObjectNameNormalizer objectNameNormalizer; readonly IDirectoryServicesConfigurationStore configurationStore; readonly IUserPrincipalFinder userPrincipalFinder; public DirectoryServicesExternalSecurityGroupLocator( - ILog log, + ISystemLog log, IDirectoryServicesContextProvider contextProvider, IDirectoryServicesObjectNameNormalizer objectNameNormalizer, IDirectoryServicesConfigurationStore configurationStore, diff --git a/source/Server/DirectoryServices/DirectoryServicesObjectNameNormalizer.cs b/source/Server/DirectoryServices/DirectoryServicesObjectNameNormalizer.cs index edc5e07..d245826 100644 --- a/source/Server/DirectoryServices/DirectoryServicesObjectNameNormalizer.cs +++ b/source/Server/DirectoryServices/DirectoryServicesObjectNameNormalizer.cs @@ -5,10 +5,10 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Director { class DirectoryServicesObjectNameNormalizer : IDirectoryServicesObjectNameNormalizer { - readonly ILog log; + readonly ISystemLog log; const string NTAccountUsernamePrefix = "nt:"; - public DirectoryServicesObjectNameNormalizer(ILog log) + public DirectoryServicesObjectNameNormalizer(ISystemLog log) { this.log = log; } diff --git a/source/Server/DirectoryServices/DirectoryServicesService.cs b/source/Server/DirectoryServices/DirectoryServicesService.cs index 791abed..9cd84ea 100644 --- a/source/Server/DirectoryServices/DirectoryServicesService.cs +++ b/source/Server/DirectoryServices/DirectoryServicesService.cs @@ -9,7 +9,7 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Director { class DirectoryServicesService : IDirectoryServicesService { - readonly ILog log; + readonly ISystemLog log; readonly IDirectoryServicesObjectNameNormalizer objectNameNormalizer; readonly IDirectoryServicesContextProvider contextProvider; @@ -27,7 +27,7 @@ class DirectoryServicesService : IDirectoryServicesService /// const int LOGON32_PROVIDER_DEFAULT = 0; - public DirectoryServicesService(ILog log, + public DirectoryServicesService(ISystemLog log, IDirectoryServicesObjectNameNormalizer objectNameNormalizer, IDirectoryServicesContextProvider contextProvider) { diff --git a/source/Server/DirectoryServices/GroupRetriever.cs b/source/Server/DirectoryServices/GroupRetriever.cs index 235a816..fed137e 100644 --- a/source/Server/DirectoryServices/GroupRetriever.cs +++ b/source/Server/DirectoryServices/GroupRetriever.cs @@ -12,12 +12,12 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Director { class GroupRetriever : IExternalGroupRetriever { - readonly ILog log; + readonly ISystemLog log; readonly IDirectoryServicesConfigurationStore configurationStore; readonly IDirectoryServicesExternalSecurityGroupLocator groupLocator; public GroupRetriever( - ILog log, + ISystemLog log, IDirectoryServicesConfigurationStore configurationStore, IDirectoryServicesExternalSecurityGroupLocator groupLocator) { diff --git a/source/Server/IntegratedAuthentication/IntegratedAuthenticationHandler.cs b/source/Server/IntegratedAuthentication/IntegratedAuthenticationHandler.cs index fa662f5..8a10ebf 100644 --- a/source/Server/IntegratedAuthentication/IntegratedAuthenticationHandler.cs +++ b/source/Server/IntegratedAuthentication/IntegratedAuthenticationHandler.cs @@ -7,7 +7,6 @@ using Newtonsoft.Json; using Octopus.Data; using Octopus.Data.Model.User; -using Octopus.CoreUtilities; using Octopus.Data.Storage.User; using Octopus.Diagnostics; using Octopus.Server.Extensibility.Authentication.DirectoryServices.DirectoryServices; @@ -34,10 +33,10 @@ public enum DomainCookieOptions CustomDomain = 0, OriginDomain = 1, } - + class IntegratedAuthenticationHandler : IIntegratedAuthenticationHandler { - readonly ILog log; + readonly ISystemLog log; readonly IWebPortalConfigurationStore configuration; readonly IAuthCookieCreator tokenIssuer; readonly IAuthenticationConfigurationStore authenticationConfigurationStore; @@ -45,7 +44,7 @@ class IntegratedAuthenticationHandler : IIntegratedAuthenticationHandler readonly IUserStore userStore; readonly IIntegratedChallengeCoordinator integratedChallengeCoordinator; - public IntegratedAuthenticationHandler(ILog log, + public IntegratedAuthenticationHandler(ISystemLog log, IWebPortalConfigurationStore configuration, IAuthCookieCreator tokenIssuer, IAuthenticationConfigurationStore authenticationConfigurationStore, @@ -61,32 +60,32 @@ public IntegratedAuthenticationHandler(ILog log, this.userStore = userStore; this.integratedChallengeCoordinator = integratedChallengeCoordinator; } - - static Maybe GetCorsOrigin(IEnumerable originHeaders, string whitelist) + + static string? GetCorsOrigin(IEnumerable originHeaders, string whitelist) { if (string.IsNullOrWhiteSpace(whitelist)) - return Maybe.None; + return null; IEnumerable headersArray = originHeaders == null ? new string[0] : originHeaders.ToArray(); if (whitelist.Equals("*")) { var firstHeader = headersArray.FirstOrDefault(); - return Maybe.Some(firstHeader ?? "*"); + return firstHeader ?? "*"; } var originHeader = headersArray.FirstOrDefault() ?? ""; var allowedDomains = whitelist.Split(','); - return allowedDomains.Contains(originHeader, StringComparer.OrdinalIgnoreCase) ? Maybe.Some(originHeader) : Maybe.None; + return allowedDomains.Contains(originHeader, StringComparer.OrdinalIgnoreCase) ? originHeader : null; } void AddCorsHeaders(HttpContext context) { context.Request.Headers.TryGetValue("Origin", out var originHeaders); var maybeAllowOrigin = GetCorsOrigin(originHeaders, configuration.GetCorsWhitelist()); - if (!maybeAllowOrigin.Some()) return; - - context.Response.Headers.Add("Access-Control-Allow-Origin", maybeAllowOrigin.Value); + if (maybeAllowOrigin == null) return; + + context.Response.Headers.Add("Access-Control-Allow-Origin", maybeAllowOrigin); context.Response.Headers.Add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS"); context.Response.Headers.Add("Access-Control-Allow-Credentials", "true"); context.Response.Headers.Add("Access-Control-Expose-Headers", @@ -103,7 +102,7 @@ public Task HandleRequest(HttpContext context) { AddCorsHeaders(context); var state = GetLoginState(context); - + if (integratedChallengeCoordinator.SetupResponseIfChallengeHasNotSucceededYet(context, state) != IntegratedChallengeTrackerStatus.ChallengeSucceeded) { // the coordinator will configure the Response object in the correct way for incomplete challenges @@ -156,19 +155,19 @@ public Task HandleRequest(HttpContext context) return Task.CompletedTask; } - + CookieOptions ConvertOctoCookieToCookieOptions(OctoCookie cookie, DomainCookieOptions options) { var result = new CookieOptions { Domain = options == DomainCookieOptions.CustomDomain ? cookie.Domain: null, - Expires = cookie.Expires, - Path = cookie.Path, + Expires = cookie.Expires, + Path = cookie.Path, HttpOnly = cookie.HttpOnly, Secure = cookie.Secure, MaxAge = cookie.MaxAge, }; - + return result; } diff --git a/source/Server/IntegratedAuthentication/IntegratedAuthenticationHost.cs b/source/Server/IntegratedAuthentication/IntegratedAuthenticationHost.cs index ba69de1..8ad7c63 100644 --- a/source/Server/IntegratedAuthentication/IntegratedAuthenticationHost.cs +++ b/source/Server/IntegratedAuthentication/IntegratedAuthenticationHost.cs @@ -32,17 +32,17 @@ namespace Octopus.Server.Extensibility.Authentication.DirectoryServices.Integrat /// and it initiates the challenge, using a 401 response, when the user isn't already authenticated. /// /// Changing the authentication scheme in this world requires a restart of all nodes, as the setting has to be set - /// when the WebHost starts. + /// when the WebHost starts. /// class IntegratedAuthenticationHost : IShareWebHostLifetime { - readonly ILog log; + readonly ISystemLog log; readonly IWebPortalConfigurationStore configuration; readonly IDirectoryServicesConfigurationStore configurationStore; readonly IIntegratedAuthenticationHandler handler; IWebHost? host; - public IntegratedAuthenticationHost(ILog log, + public IntegratedAuthenticationHost(ISystemLog log, IWebPortalConfigurationStore configuration, IDirectoryServicesConfigurationStore configurationStore, IIntegratedAuthenticationHandler handler) @@ -64,29 +64,29 @@ public Task StartAsync() var prefixes = GetListenPrefixes(); var builder = new WebHostBuilder(); - + builder.UseHttpSys(options => { options.Authentication.Schemes = MapAuthenticationScheme(); - + // IMPORTANT: we need AllowAnonymous to be true here. If it is false then the ASPNET Core internals // will automatically issue the 401 challenge and we don't get a chance to report meaningful errors // if the challenge fails (the user will get the challenge popup dialog in the browser). options.Authentication.AllowAnonymous = true; - + foreach (var baseUri in prefixes) { var prefix = baseUri.ToString(); - + if (!baseUri.Host.Contains(".")) { prefix = prefix.Replace("localhost", "+"); } - + options.UrlPrefixes.Add(prefix); } }); - + builder.Configure(app => { app.Use((context, func) => @@ -100,7 +100,7 @@ public Task StartAsync() return handler.HandleRequest(context); }); }); - + host = builder.Build(); return host.StartAsync(); } diff --git a/source/Server/Server.csproj b/source/Server/Server.csproj index 5bbccdb..45fa6c2 100644 --- a/source/Server/Server.csproj +++ b/source/Server/Server.csproj @@ -19,9 +19,9 @@ - - - + + +