Skip to content

Commit 5255547

Browse files
authored
Merge pull request #938 from mysql-net/server-redirection
Add Azure server redirection.
2 parents 28ad1d3 + 1247d06 commit 5255547

File tree

3 files changed

+132
-8
lines changed

3 files changed

+132
-8
lines changed

src/MySqlConnector/Core/ConnectionSettings.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
150150
static int ToSigned(uint value) => value >= int.MaxValue ? int.MaxValue : (int) value;
151151
}
152152

153+
public ConnectionSettings CloneWith(string host, int port, string userId, bool isRedirected) => new ConnectionSettings(this, host, port, userId, isRedirected);
154+
153155
private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat, bool oldGuids)
154156
{
155157
switch (guidFormat)
@@ -240,6 +242,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
240242
public bool UseXaTransactions { get; }
241243

242244
public byte[]? ConnectionAttributes { get; set; }
245+
public bool IsRedirected { get; }
243246

244247
// Helper Functions
245248
int? m_connectionTimeoutMilliseconds;
@@ -265,6 +268,66 @@ public int ConnectionTimeoutMilliseconds
265268
}
266269
}
267270

271+
private ConnectionSettings(ConnectionSettings other, string host, int port, string userId, bool isRedirected)
272+
{
273+
ConnectionStringBuilder = other.ConnectionStringBuilder;
274+
ConnectionString = other.ConnectionString;
275+
276+
ConnectionProtocol = MySqlConnectionProtocol.Sockets;
277+
HostNames = new[] { host };
278+
LoadBalance = other.LoadBalance;
279+
Port = port;
280+
PipeName = other.PipeName;
281+
282+
UserID = userId;
283+
Password = other.Password;
284+
Database = other.Database;
285+
286+
SslMode = other.SslMode;
287+
CertificateFile = other.CertificateFile;
288+
CertificatePassword = other.CertificatePassword;
289+
SslCertificateFile = other.SslCertificateFile;
290+
SslKeyFile = other.SslKeyFile;
291+
CACertificateFile = other.CACertificateFile;
292+
CertificateStoreLocation = other.CertificateStoreLocation;
293+
CertificateThumbprint = other.CertificateThumbprint;
294+
295+
Pooling = other.Pooling;
296+
ConnectionLifeTime = other.ConnectionLifeTime;
297+
ConnectionReset = other.ConnectionReset;
298+
ConnectionIdlePingTime = other.ConnectionIdlePingTime;
299+
ConnectionIdleTimeout = other.ConnectionIdleTimeout;
300+
MinimumPoolSize = other.MinimumPoolSize;
301+
MaximumPoolSize = other.MaximumPoolSize;
302+
303+
AllowLoadLocalInfile = other.AllowLoadLocalInfile;
304+
AllowPublicKeyRetrieval = other.AllowPublicKeyRetrieval;
305+
AllowUserVariables = other.AllowUserVariables;
306+
AllowZeroDateTime = other.AllowZeroDateTime;
307+
ApplicationName = other.ApplicationName;
308+
AutoEnlist = other.AutoEnlist;
309+
ConnectionTimeout = other.ConnectionTimeout;
310+
ConvertZeroDateTime = other.ConvertZeroDateTime;
311+
DateTimeKind = other.DateTimeKind;
312+
DefaultCommandTimeout = other.DefaultCommandTimeout;
313+
ForceSynchronous = other.ForceSynchronous;
314+
IgnoreCommandTransaction = other.IgnoreCommandTransaction;
315+
IgnorePrepare = other.IgnorePrepare;
316+
InteractiveSession = other.InteractiveSession;
317+
GuidFormat = other.GuidFormat;
318+
Keepalive = other.Keepalive;
319+
NoBackslashEscapes = other.NoBackslashEscapes;
320+
PersistSecurityInfo = other.PersistSecurityInfo;
321+
ServerRsaPublicKeyFile = other.ServerRsaPublicKeyFile;
322+
ServerSPN = other.ServerSPN;
323+
TreatTinyAsBoolean = other.TreatTinyAsBoolean;
324+
UseAffectedRows = other.UseAffectedRows;
325+
UseCompression = other.UseCompression;
326+
UseXaTransactions = other.UseXaTransactions;
327+
328+
IsRedirected = isRedirected;
329+
}
330+
268331
static readonly string[] s_localhostPipeServer = { "." };
269332
}
270333
}

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ public async Task ConnectAsync(ConnectionSettings cs, int startTickCount, ILoadB
350350
m_state = State.Connecting;
351351
}
352352

353+
serverRedirection:
353354
// TLS negotiation should automatically fall back to the best version supported by client and server. However,
354355
// Windows Schannel clients will fail to connect to a yaSSL-based MySQL Server if TLS 1.2 is requested and
355356
// have to use only TLS 1.1: https://github.com/mysql-net/MySqlConnector/pull/101
@@ -465,7 +466,51 @@ public async Task ConnectAsync(ConnectionSettings cs, int startTickCount, ILoadB
465466
payload = await SwitchAuthenticationAsync(cs, payload, ioBehavior, cancellationToken).ConfigureAwait(false);
466467
}
467468

468-
OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
469+
var ok = OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
470+
if (ok.StatusInfo is not null && ok.StatusInfo.StartsWith("Location: mysql://", StringComparison.Ordinal))
471+
{
472+
// server redirection string has the format "Location: mysql://{host}:{port}/user={userId}[&ttl={ttl}]"
473+
m_logArguments[1] = ok.StatusInfo;
474+
Log.Info("Session{0} has server redirection header {1}", m_logArguments);
475+
476+
if (cs.IsRedirected)
477+
{
478+
Log.Info("Session{0} is already redirected; ignoring it.", m_logArguments);
479+
}
480+
else
481+
{
482+
var hostIndex = 18;
483+
var colonIndex = ok.StatusInfo.IndexOf(':', hostIndex);
484+
if (colonIndex != -1)
485+
{
486+
var host = ok.StatusInfo.Substring(hostIndex, colonIndex - hostIndex);
487+
var portIndex = colonIndex + 1;
488+
var userIndex = ok.StatusInfo.IndexOf("/user=", StringComparison.Ordinal);
489+
if (userIndex != -1)
490+
{
491+
if (int.TryParse(ok.StatusInfo.Substring(portIndex, userIndex - portIndex), out var port))
492+
{
493+
var ampersandIndex = ok.StatusInfo.IndexOf('&', userIndex);
494+
var userId = ampersandIndex == -1 ? ok.StatusInfo.Substring(userIndex + 6) : ok.StatusInfo.Substring(userIndex + 6, ampersandIndex - userIndex - 6);
495+
Log.Info("Session{0} found server redirection Host={1}; Port={2}; User={3}", m_logArguments[0], host, port, userId);
496+
497+
if (host != cs.HostNames![0] || port != cs.Port || userId != cs.UserID)
498+
{
499+
Log.Info("Session{0} closing existing connection", m_logArguments);
500+
await SendAsync(QuitPayload.Instance, ioBehavior, cancellationToken).ConfigureAwait(false);
501+
Log.Info("Session{0} opening new connection to Host={1}; Port={2}; User={3}", m_logArguments[0], host, port, userId);
502+
cs = cs.CloneWith(host, port, userId, isRedirected: true);
503+
goto serverRedirection;
504+
}
505+
else
506+
{
507+
Log.Info("Session{0} is already connected to this server; ignoring redirection", m_logArguments);
508+
}
509+
}
510+
}
511+
}
512+
}
513+
}
469514

470515
if (m_useCompression)
471516
m_payloadHandler = new CompressedPayloadHandler(m_payloadHandler.ByteHandler);
@@ -1420,6 +1465,7 @@ private bool ShouldGetRealServerDetails(ConnectionSettings cs)
14201465
if (cs.ConnectionProtocol == MySqlConnectionProtocol.Sockets && cs.HostNames!.Count == 1)
14211466
{
14221467
return cs.HostNames[0].EndsWith(".mysql.database.azure.com", StringComparison.OrdinalIgnoreCase) ||
1468+
cs.HostNames[0].EndsWith(".database.windows.net", StringComparison.OrdinalIgnoreCase) ||
14231469
cs.HostNames[0].EndsWith(".mysql.database.chinacloudapi.cn", StringComparison.OrdinalIgnoreCase);
14241470
}
14251471

src/MySqlConnector/Protocol/Payloads/OkPayload.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal sealed class OkPayload
1111
public ulong LastInsertId { get; }
1212
public ServerStatus ServerStatus { get; }
1313
public int WarningCount { get; }
14+
public string? StatusInfo { get; }
1415
public string? NewSchema { get; }
1516

1617
public const byte Signature = 0x00;
@@ -36,12 +37,13 @@ public static OkPayload Create(ReadOnlySpan<byte> span, bool deprecateEof, bool
3637
var serverStatus = (ServerStatus) reader.ReadUInt16();
3738
var warningCount = (int) reader.ReadUInt16();
3839
string? newSchema = null;
40+
ReadOnlySpan<byte> statusBytes;
3941

4042
if (clientSessionTrack)
4143
{
4244
if (reader.BytesRemaining > 0)
4345
{
44-
reader.ReadLengthEncodedByteString(); // human-readable info
46+
statusBytes = reader.ReadLengthEncodedByteString(); // human-readable info
4547

4648
if ((serverStatus & ServerStatus.SessionStateChanged) == ServerStatus.SessionStateChanged && reader.BytesRemaining > 0)
4749
{
@@ -65,33 +67,46 @@ public static OkPayload Create(ReadOnlySpan<byte> span, bool deprecateEof, bool
6567
}
6668
}
6769
}
70+
else
71+
{
72+
statusBytes = default;
73+
}
6874
}
6975
else
7076
{
71-
// ignore "string<EOF> info" human-readable string
77+
// read EOF-terminated string
78+
statusBytes = reader.ReadByteString(reader.BytesRemaining);
79+
80+
// try to detect if it was actually a length-prefixed string (up to 250 bytes); some servers send
81+
// a length-prefixed status string even when CLIENT_SESSION_TRACK is not specified
82+
if (statusBytes.Length != 0 && statusBytes[0] == statusBytes.Length - 1)
83+
statusBytes = statusBytes.Slice(1);
7284
}
7385

74-
if (affectedRowCount == 0 && lastInsertId == 0 && warningCount == 0 && newSchema is null)
86+
var statusInfo = statusBytes.Length == 0 ? null : Encoding.UTF8.GetString(statusBytes);
87+
88+
if (affectedRowCount == 0 && lastInsertId == 0 && warningCount == 0 && statusInfo is null && newSchema is null)
7589
{
7690
if (serverStatus == ServerStatus.AutoCommit)
7791
return s_autoCommitOk;
7892
if (serverStatus == (ServerStatus.AutoCommit | ServerStatus.SessionStateChanged))
7993
return s_autoCommitSessionStateChangedOk;
8094
}
8195

82-
return new OkPayload(affectedRowCount, lastInsertId, serverStatus, warningCount, newSchema);
96+
return new OkPayload(affectedRowCount, lastInsertId, serverStatus, warningCount, statusInfo, newSchema);
8397
}
8498

85-
private OkPayload(int affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? newSchema)
99+
private OkPayload(int affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema)
86100
{
87101
AffectedRowCount = affectedRowCount;
88102
LastInsertId = lastInsertId;
89103
ServerStatus = serverStatus;
90104
WarningCount = warningCount;
105+
StatusInfo = statusInfo;
91106
NewSchema = newSchema;
92107
}
93108

94-
static readonly OkPayload s_autoCommitOk = new(0, 0, ServerStatus.AutoCommit, 0, null);
95-
static readonly OkPayload s_autoCommitSessionStateChangedOk = new(0, 0, ServerStatus.AutoCommit | ServerStatus.SessionStateChanged, 0, null);
109+
static readonly OkPayload s_autoCommitOk = new(0, 0, ServerStatus.AutoCommit, 0, null, null);
110+
static readonly OkPayload s_autoCommitSessionStateChangedOk = new(0, 0, ServerStatus.AutoCommit | ServerStatus.SessionStateChanged, 0, null, null);
96111
}
97112
}

0 commit comments

Comments
 (0)