// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Net; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using osu.Framework; using osu.Game.Online.API; namespace osu.Game.Online { public class HubClientConnector : PersistentEndpointClientConnector, IHubClientConnector { public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down."; public const string VERSION_HASH_HEADER = @"X-Osu-Version-Hash"; public const string CLIENT_SESSION_ID_HEADER = @"X-Client-Session-ID"; /// /// Invoked whenever a new hub connection is built, to configure it before it's started. /// public Action? ConfigureConnection { get; set; } private readonly string endpoint; private readonly string versionHash; private readonly bool preferMessagePack; /// /// The current connection opened by this connector. /// public new HubConnection? CurrentConnection => ((HubClient?)base.CurrentConnection)?.Connection; /// /// Constructs a new . /// /// The name of the client this connector connects for, used for logging. /// The endpoint to the hub. /// An API provider used to react to connection state changes. /// The hash representing the current game version, used for verification purposes. /// Whether to use MessagePack for serialisation if available on this platform. public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true) : base(api) { ClientName = clientName; this.endpoint = endpoint; this.versionHash = versionHash; this.preferMessagePack = preferMessagePack; // Automatically start these connections. Start(); } protected override Task BuildConnectionAsync(CancellationToken cancellationToken) { var builder = new HubConnectionBuilder() .WithUrl(endpoint, options => { // Configuring proxies is not supported on iOS, see https://github.com/xamarin/xamarin-macios/issues/14632. if (RuntimeInfo.OS != RuntimeInfo.Platform.iOS) { // Use HttpClient.DefaultProxy once on net6 everywhere. // The credential setter can also be removed at this point. options.Proxy = WebRequest.DefaultWebProxy; if (options.Proxy != null) options.Proxy.Credentials = CredentialCache.DefaultCredentials; } options.Headers.Add(@"Authorization", @$"Bearer {API.AccessToken}"); // non-standard header name kept for backwards compatibility, can be removed after server side has migrated to `VERSION_HASH_HEADER` options.Headers.Add(@"OsuVersionHash", versionHash); options.Headers.Add(VERSION_HASH_HEADER, versionHash); options.Headers.Add(CLIENT_SESSION_ID_HEADER, API.SessionIdentifier.ToString()); }); if (RuntimeFeature.IsDynamicCodeCompiled && preferMessagePack) { builder.AddMessagePackProtocol(options => { options.SerializerOptions = SignalRUnionWorkaroundResolver.OPTIONS; }); } else { // eventually we will precompile resolvers for messagepack, but this isn't working currently // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; options.PayloadSerializerSettings.Converters = new List { new SignalRDerivedTypeWorkaroundJsonConverter(), }; }); } var newConnection = builder.Build(); ConfigureConnection?.Invoke(newConnection); return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection)); } async Task IHubClientConnector.Disconnect() { await Disconnect().ConfigureAwait(false); API.Logout(); } protected override string ClientName { get; } } }