1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 12:47:25 +08:00
osu-lazer/osu.Game/Online/HubClientConnector.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

118 lines
5.1 KiB
C#
Raw Normal View History

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
2022-11-02 10:43:22 +08:00
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";
Send client-generated session GUID for identification purposes This is the first half of a change that *may* fix https://github.com/ppy/osu/issues/26338 (it definitely fixes *one case* where the issue happens, but I'm not sure if it will cover all of them). As described in the issue thread, using the `jti` claim from the JWT used for authorisation seemed like a decent idea. However, upon closer inspection the scheme falls over badly in a specific scenario where: 1. A client instance connects to spectator server using JWT A. 2. At some point, JWT A expires, and is silently rotated by the game in exchange for JWT B. The spectator server knows nothing of this, and continues to only track JWT A, including the old `jti` claim in said JWT. 3. At some later point, the client's connection to one of the spectator server hubs drops out. A reconnection is automatically attempted, *but* it is attempted using JWT B. The spectator server was not aware of JWT B until now, and said JWT has a different `jti` claim than the old one, so to the spectator server, it looks like a completely different client connecting, which boots the user out of their account. This PR adds a per-session GUID which is sent in a HTTP header on every connection attempt to spectator server. This GUID will be used instead of the `jti` claim in JWTs as a persistent identifier of a single user's single lazer session, which bypasses the failure scenario described above. I don't think any stronger primitive than this is required. As far as I can tell this is as strong a protection as the JWT was (which is to say, not *very* strong), and doing this removes a lot of weird complexity that would be otherwise incurred by attempting to have client ferry all of its newly issued JWTs to the server so that it can be aware of them.
2024-07-17 20:11:35 +08:00
public const string CLIENT_SESSION_ID_HEADER = @"X-Client-Session-ID";
/// <summary>
/// Invoked whenever a new hub connection is built, to configure it before it's started.
/// </summary>
public Action<HubConnection>? ConfigureConnection { get; set; }
private readonly string endpoint;
2021-02-14 22:31:57 +08:00
private readonly string versionHash;
private readonly bool preferMessagePack;
/// <summary>
/// The current connection opened by this connector.
/// </summary>
public new HubConnection? CurrentConnection => ((HubClient?)base.CurrentConnection)?.Connection;
/// <summary>
/// Constructs a new <see cref="HubClientConnector"/>.
/// </summary>
/// <param name="clientName">The name of the client this connector connects for, used for logging.</param>
/// <param name="endpoint">The endpoint to the hub.</param>
/// <param name="api"> An API provider used to react to connection state changes.</param>
2021-02-14 22:31:57 +08:00
/// <param name="versionHash">The hash representing the current game version, used for verification purposes.</param>
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
: base(api)
{
ClientName = clientName;
this.endpoint = endpoint;
2021-02-14 22:31:57 +08:00
this.versionHash = versionHash;
this.preferMessagePack = preferMessagePack;
2022-11-04 18:36:24 +08:00
// Automatically start these connections.
Start();
}
2022-11-02 10:43:22 +08:00
protected override Task<PersistentEndpointClient> BuildConnectionAsync(CancellationToken cancellationToken)
{
var builder = new HubConnectionBuilder()
2021-02-14 22:31:57 +08:00
.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;
}
Send client-generated session GUID for identification purposes This is the first half of a change that *may* fix https://github.com/ppy/osu/issues/26338 (it definitely fixes *one case* where the issue happens, but I'm not sure if it will cover all of them). As described in the issue thread, using the `jti` claim from the JWT used for authorisation seemed like a decent idea. However, upon closer inspection the scheme falls over badly in a specific scenario where: 1. A client instance connects to spectator server using JWT A. 2. At some point, JWT A expires, and is silently rotated by the game in exchange for JWT B. The spectator server knows nothing of this, and continues to only track JWT A, including the old `jti` claim in said JWT. 3. At some later point, the client's connection to one of the spectator server hubs drops out. A reconnection is automatically attempted, *but* it is attempted using JWT B. The spectator server was not aware of JWT B until now, and said JWT has a different `jti` claim than the old one, so to the spectator server, it looks like a completely different client connecting, which boots the user out of their account. This PR adds a per-session GUID which is sent in a HTTP header on every connection attempt to spectator server. This GUID will be used instead of the `jti` claim in JWTs as a persistent identifier of a single user's single lazer session, which bypasses the failure scenario described above. I don't think any stronger primitive than this is required. As far as I can tell this is as strong a protection as the JWT was (which is to say, not *very* strong), and doing this removes a lot of weird complexity that would be otherwise incurred by attempting to have client ferry all of its newly issued JWTs to the server so that it can be aware of them.
2024-07-17 20:11:35 +08:00
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);
Send client-generated session GUID for identification purposes This is the first half of a change that *may* fix https://github.com/ppy/osu/issues/26338 (it definitely fixes *one case* where the issue happens, but I'm not sure if it will cover all of them). As described in the issue thread, using the `jti` claim from the JWT used for authorisation seemed like a decent idea. However, upon closer inspection the scheme falls over badly in a specific scenario where: 1. A client instance connects to spectator server using JWT A. 2. At some point, JWT A expires, and is silently rotated by the game in exchange for JWT B. The spectator server knows nothing of this, and continues to only track JWT A, including the old `jti` claim in said JWT. 3. At some later point, the client's connection to one of the spectator server hubs drops out. A reconnection is automatically attempted, *but* it is attempted using JWT B. The spectator server was not aware of JWT B until now, and said JWT has a different `jti` claim than the old one, so to the spectator server, it looks like a completely different client connecting, which boots the user out of their account. This PR adds a per-session GUID which is sent in a HTTP header on every connection attempt to spectator server. This GUID will be used instead of the `jti` claim in JWTs as a persistent identifier of a single user's single lazer session, which bypasses the failure scenario described above. I don't think any stronger primitive than this is required. As far as I can tell this is as strong a protection as the JWT was (which is to say, not *very* strong), and doing this removes a lot of weird complexity that would be otherwise incurred by attempting to have client ferry all of its newly issued JWTs to the server so that it can be aware of them.
2024-07-17 20:11:35 +08:00
options.Headers.Add(VERSION_HASH_HEADER, versionHash);
options.Headers.Add(CLIENT_SESSION_ID_HEADER, API.SessionIdentifier.ToString());
2021-02-14 22:31:57 +08:00
});
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<JsonConverter>
{
new SignalRDerivedTypeWorkaroundJsonConverter(),
};
});
}
var newConnection = builder.Build();
ConfigureConnection?.Invoke(newConnection);
2022-11-02 10:43:22 +08:00
return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection));
}
async Task IHubClientConnector.Disconnect()
{
await Disconnect().ConfigureAwait(false);
API.Logout();
}
protected override string ClientName { get; }
}
}