Skip to content

Commit

Permalink
Basic implementation of HELLO (#398)
Browse files Browse the repository at this point in the history
* Basic implementation of HELLO command

* fix

* Added command info for HELLO + added website docs

* dotnet format

* update SE.Redis to latest, add HELLO unit test

* Add and use EqualsIgnoreCase

* use nameof(RespCommand.HELLO)

* updates

* support special pong in RESP2 subscribe

* only support RESP2 in this PR

* remove resp3 testcase

* fix warnings

* fix warning

* make EqualsIgnoreCase extension method in utils

* improve the case-insensitive match test

* nit

---------

Co-authored-by: Tal Zaccai <[email protected]>
  • Loading branch information
badrishc and TalZaccai authored May 20, 2024
1 parent b1891d1 commit 5a65fd7
Show file tree
Hide file tree
Showing 22 changed files with 363 additions and 75 deletions.
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="7.5.1" />
<PackageVersion Include="Microsoft.IdentityModel.Validators" Version="7.5.1" />
<PackageVersion Include="StackExchange.Redis" Version="2.6.80" />
<PackageVersion Include="StackExchange.Redis" Version="2.7.33" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.5.1" />
<PackageVersion Include="System.Interactive.Async" Version="6.0.1" />
</ItemGroup>
Expand Down
34 changes: 34 additions & 0 deletions libs/common/ConvertUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.

using System;
using System.Diagnostics;

namespace Garnet.common
{
Expand Down Expand Up @@ -53,5 +54,38 @@ public static void MakeUpperCase(Span<byte> command)
if (c > 96 && c < 123)
c -= 32;
}

/// <summary>
/// Check if two byte spans are equal, where right is an all-upper-case span, ignoring case if there are ASCII bytes.
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool EqualsUpperCaseSpanIgnoringCase(this ReadOnlySpan<byte> left, ReadOnlySpan<byte> right)
{
if (left.SequenceEqual(right))
return true;
if (left.Length != right.Length)
return false;
for (int i = 0; i < left.Length; i++)
{
var b1 = left[i];
var b2 = right[i];

// Debug assert that b2 is an upper case letter 'A'-'Z'
Debug.Assert(b2 is >= 65 and <= 90);

if (b1 == b2 || b1 - 32 == b2)
continue;
return false;
}
return true;
}

/// <summary>
/// Check if two byte spans are equal, where right is an all-upper-case span, ignoring case if there are ASCII bytes.
/// </summary>
public static bool EqualsUpperCaseSpanIgnoringCase(this Span<byte> left, ReadOnlySpan<byte> right)
=> EqualsUpperCaseSpanIgnoringCase((ReadOnlySpan<byte>)left, right);
}
}
15 changes: 15 additions & 0 deletions libs/common/RespWriteUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ namespace Garnet.common
/// </summary>
public static unsafe class RespWriteUtils
{
/// <summary>
/// Write map length
/// </summary>
public static bool WriteMapLength(int len, ref byte* curr, byte* end)
{
int numDigits = NumUtils.NumDigits(len);
int totalLen = 1 + numDigits + 2;
if (totalLen > (int)(end - curr))
return false;
*curr++ = (byte)'%';
NumUtils.IntToBytes(len, numDigits, ref curr);
WriteNewline(ref curr);
return true;
}

/// <summary>
/// Write array length
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions libs/server/Metrics/Info/GarnetInfoMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ private void PopulateServerInfo(StoreWrapper storeWrapper)
serverInfo =
[
new("garnet_version", storeWrapper.version),
new("garnet_mode", storeWrapper.serverOptions.EnableCluster ? "cluster" : "standalone"),
new("os", Environment.OSVersion.ToString()),
new("processor_count", Environment.ProcessorCount.ToString()),
new("arch_bits", Environment.Is64BitProcess ? "64" : "32"),
Expand All @@ -56,7 +55,8 @@ private void PopulateServerInfo(StoreWrapper storeWrapper)
new("monitor_freq", storeWrapper.serverOptions.MetricsSamplingFrequency.ToString()),
new("latency_monitor", storeWrapper.serverOptions.LatencyMonitor ? "enabled" : "disabled"),
new("run_id", storeWrapper.run_id),
new("redis_version", storeWrapper.redisProtocolVersion)
new("redis_version", storeWrapper.redisProtocolVersion),
new("redis_mode", storeWrapper.serverOptions.EnableCluster ? "cluster" : "standalone"),
];
}

Expand Down
155 changes: 151 additions & 4 deletions libs/server/Resp/AdminCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ private bool ProcessAdminCommands<TGarnetApi>(RespCommand command, ReadOnlySpan<
{
if (username.IsEmpty)
{
while (!RespWriteUtils.WriteError("WRONGPASS Invalid password"u8, ref dcurr, dend))
while (!RespWriteUtils.WriteError(CmdStrings.RESP_WRONGPASS_INVALID_PASSWORD, ref dcurr, dend))
SendAndReset();
}
else
{
while (!RespWriteUtils.WriteError("WRONGPASS Invalid username/password combination"u8, ref dcurr, dend))
while (!RespWriteUtils.WriteError(CmdStrings.RESP_WRONGPASS_INVALID_USERNAME_PASSWORD, ref dcurr, dend))
SendAndReset();
}
}
Expand Down Expand Up @@ -250,8 +250,16 @@ private bool ProcessAdminCommands<TGarnetApi>(RespCommand command, ReadOnlySpan<
{
if (count == 0)
{
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_PONG, ref dcurr, dend))
SendAndReset();
if (isSubscriptionSession && respProtocolVersion == 2)
{
while (!RespWriteUtils.WriteDirect(CmdStrings.SUSCRIBE_PONG, ref dcurr, dend))
SendAndReset();
}
else
{
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_PONG, ref dcurr, dend))
SendAndReset();
}
}
else if (count == 1)
{
Expand All @@ -270,6 +278,74 @@ private bool ProcessAdminCommands<TGarnetApi>(RespCommand command, ReadOnlySpan<
errorCmd = "ping";
}
}
else if (command == RespCommand.HELLO)
{
int? respProtocolVersion = null;
ReadOnlySpan<byte> authUsername = default, authPassword = default;
string clientName = null;

if (count > 0)
{
var ptr = recvBufferPtr + readHead;
int localRespProtocolVersion;
if (!RespReadUtils.ReadIntWithLengthHeader(out localRespProtocolVersion, ref ptr, recvBufferPtr + bytesRead))
return false;
readHead = (int)(ptr - recvBufferPtr);

respProtocolVersion = localRespProtocolVersion;
count--;
while (count > 0)
{
var param = GetCommand(bufSpan, out bool success1);
if (!success1) return false;
count--;
if (param.EqualsUpperCaseSpanIgnoringCase(CmdStrings.AUTH))
{
if (count < 2)
{
if (!DrainCommands(bufSpan, count))
return false;
count = 0;
errorFlag = true;
errorCmd = nameof(RespCommand.HELLO);
break;
}
authUsername = GetCommand(bufSpan, out success1);
if (!success1) return false;
count--;
authPassword = GetCommand(bufSpan, out success1);
if (!success1) return false;
count--;
}
else if (param.EqualsUpperCaseSpanIgnoringCase(CmdStrings.SETNAME))
{
if (count < 1)
{
if (!DrainCommands(bufSpan, count))
return false;
count = 0;
errorFlag = true;
errorCmd = nameof(RespCommand.HELLO);
break;
}

var arg = GetCommand(bufSpan, out success1);
if (!success1) return false;
count--;
clientName = Encoding.ASCII.GetString(arg);
}
else
{
if (!DrainCommands(bufSpan, count))
return false;
count = 0;
errorFlag = true;
errorCmd = nameof(RespCommand.HELLO);
}
}
}
if (!errorFlag) ProcessHelloCommand(respProtocolVersion, authUsername, authPassword, clientName);
}
else if (command is RespCommand.CLUSTER or RespCommand.MIGRATE or RespCommand.FAILOVER or RespCommand.REPLICAOF or RespCommand.SECONDARYOF)
{
if (clusterSession == null)
Expand Down Expand Up @@ -488,6 +564,77 @@ private bool ProcessAdminCommands<TGarnetApi>(RespCommand command, ReadOnlySpan<
return true;
}

void ProcessHelloCommand(int? respProtocolVersion, ReadOnlySpan<byte> username, ReadOnlySpan<byte> password, string clientName)
{
if (respProtocolVersion != null)
{
if (respProtocolVersion.Value != 2)
{
while (!RespWriteUtils.WriteError(CmdStrings.RESP_ERR_UNSUPPORTED_PROTOCOL_VERSION, ref dcurr, dend))
SendAndReset();
return;
}

this.respProtocolVersion = respProtocolVersion.Value;
}

if (username != default)
{
if (!this.AuthenticateUser(username, password))
{
if (username.IsEmpty)
{
while (!RespWriteUtils.WriteError(CmdStrings.RESP_WRONGPASS_INVALID_PASSWORD, ref dcurr, dend))
SendAndReset();
}
else
{
while (!RespWriteUtils.WriteError(CmdStrings.RESP_WRONGPASS_INVALID_USERNAME_PASSWORD, ref dcurr, dend))
SendAndReset();
}
return;
}
}

if (clientName != null)
{
this.clientName = clientName;
}

(string, string)[] helloResult =
[
("server", "redis"),
("version", storeWrapper.redisProtocolVersion),
("garnet_version", storeWrapper.version),
("proto", $"{this.respProtocolVersion}"),
("id", "63"),
("mode", storeWrapper.serverOptions.EnableCluster ? "cluster" : "standalone"),
("role", storeWrapper.serverOptions.EnableCluster && storeWrapper.clusterProvider.IsReplica() ? "replica" : "master"),
];

if (this.respProtocolVersion == 2)
{
while (!RespWriteUtils.WriteArrayLength(helloResult.Length * 2 + 2, ref dcurr, dend))
SendAndReset();
}
else
{
while (!RespWriteUtils.WriteMapLength(helloResult.Length + 1, ref dcurr, dend))
SendAndReset();
}
for (int i = 0; i < helloResult.Length; i++)
{
while (!RespWriteUtils.WriteAsciiBulkString(helloResult[i].Item1, ref dcurr, dend))
SendAndReset();
while (!RespWriteUtils.WriteAsciiBulkString(helloResult[i].Item2, ref dcurr, dend))
SendAndReset();
}
while (!RespWriteUtils.WriteAsciiBulkString("modules", ref dcurr, dend))
SendAndReset();
while (!RespWriteUtils.WriteArrayLength(0, ref dcurr, dend))
SendAndReset();
}

/// <summary>
/// Performs @admin command group permission checks for the current user and the given command.
/// (NOTE: This function is temporary until per-command permissions are implemented)
Expand Down
12 changes: 10 additions & 2 deletions libs/server/Resp/BasicCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -888,8 +888,16 @@ private bool NetworkAppend<TGarnetApi>(byte* ptr, ref TGarnetApi storageApi)
/// </summary>
private bool NetworkPING()
{
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_PONG, ref dcurr, dend))
SendAndReset();
if (isSubscriptionSession && respProtocolVersion == 2)
{
while (!RespWriteUtils.WriteDirect(CmdStrings.SUSCRIBE_PONG, ref dcurr, dend))
SendAndReset();
}
else
{
while (!RespWriteUtils.WriteDirect(CmdStrings.RESP_PONG, ref dcurr, dend))
SendAndReset();
}
return true;
}

Expand Down
9 changes: 8 additions & 1 deletion libs/server/Resp/CmdStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> ACL => "ACL"u8;
public static ReadOnlySpan<byte> AUTH => "AUTH"u8;
public static ReadOnlySpan<byte> auth => "auth"u8;
public static ReadOnlySpan<byte> SETNAME => "SETNAME"u8;
public static ReadOnlySpan<byte> INFO => "INFO"u8;
public static ReadOnlySpan<byte> info => "info"u8;
public static ReadOnlySpan<byte> DOCS => "DOCS"u8;
Expand All @@ -47,6 +48,7 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> HELP => "HELP"u8;
public static ReadOnlySpan<byte> help => "help"u8;
public static ReadOnlySpan<byte> PING => "PING"u8;
public static ReadOnlySpan<byte> HELLO => "HELLO"u8;
public static ReadOnlySpan<byte> TIME => "TIME"u8;
public static ReadOnlySpan<byte> RESET => "RESET"u8;
public static ReadOnlySpan<byte> reset => "reset"u8;
Expand Down Expand Up @@ -80,12 +82,13 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> RESP_RETURN_VAL_1 => ":1\r\n"u8;
public static ReadOnlySpan<byte> RESP_RETURN_VAL_0 => ":0\r\n"u8;
public static ReadOnlySpan<byte> RESP_RETURN_VAL_N1 => ":-1\r\n"u8;
public static ReadOnlySpan<byte> SUSCRIBE_PONG => "*2\r\n$4\r\npong\r\n$0\r\n\r\n"u8;
public static ReadOnlySpan<byte> RESP_PONG => "+PONG\r\n"u8;
public static ReadOnlySpan<byte> RESP_EMPTY => "$0\r\n\r\n"u8;
public static ReadOnlySpan<byte> RESP_QUEUED => "+QUEUED\r\n"u8;

/// <summary>
/// Simple error respone strings, i.e. these are of the form "-errorString\r\n"
/// Simple error response strings, i.e. these are of the form "-errorString\r\n"
/// </summary>
public static ReadOnlySpan<byte> RESP_ERR_NOAUTH => "NOAUTH Authentication required."u8;
public static ReadOnlySpan<byte> RESP_ERR_WRONG_TYPE => "WRONGTYPE Operation against a key holding the wrong kind of value."u8;
Expand Down Expand Up @@ -119,6 +122,10 @@ static partial class CmdStrings
public static ReadOnlySpan<byte> RESP_ERR_GENERIC_INDEX_OUT_RANGE => "ERR index out of range"u8;
public static ReadOnlySpan<byte> RESP_ERR_GENERIC_SELECT_INVALID_INDEX => "ERR invalid database index."u8;
public static ReadOnlySpan<byte> RESP_ERR_GENERIC_SELECT_CLUSTER_MODE => "ERR SELECT is not allowed in cluster mode"u8;
public static ReadOnlySpan<byte> RESP_ERR_UNSUPPORTED_PROTOCOL_VERSION => "ERR Unsupported protocol version"u8;
public static ReadOnlySpan<byte> RESP_WRONGPASS_INVALID_PASSWORD => "WRONGPASS Invalid password"u8;
public static ReadOnlySpan<byte> RESP_WRONGPASS_INVALID_USERNAME_PASSWORD => "WRONGPASS Invalid username/password combination"u8;


/// <summary>
/// Response string templates
Expand Down
5 changes: 5 additions & 0 deletions libs/server/Resp/RespCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public enum RespCommand : byte
FORCEGC = 0x3B,
FAILOVER = 0x3C,
ACL = 0x3D,
HELLO = 0x3E,

// Custom commands
CustomTxn = 0x29,
Expand Down Expand Up @@ -1048,6 +1049,10 @@ static RespCommand MatchedNone(RespServerSession session, int oldReadHead)
{
return (RespCommand.PING, 0);
}
else if (command.SequenceEqual(CmdStrings.HELLO))
{
return (RespCommand.HELLO, 0);
}
else if (command.SequenceEqual(CmdStrings.CLUSTER))
{
return (RespCommand.CLUSTER, 0);
Expand Down
15 changes: 15 additions & 0 deletions libs/server/Resp/RespCommandsInfo.json
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,21 @@
],
"SubCommands": null
},
{
"Command": "HELLO",
"ArrayCommand": null,
"Name": "HELLO",
"IsInternal": false,
"Arity": -1,
"Flags": "Fast, Loading, NoAuth, NoScript, Stale, AllowBusy",
"FirstKey": 0,
"LastKey": 0,
"Step": 0,
"AclCategories": "Connection, Fast",
"Tips": null,
"KeySpecifications": null,
"SubCommands": null
},
{
"Command": "Hash",
"ArrayCommand": 7,
Expand Down
4 changes: 4 additions & 0 deletions libs/server/Resp/RespServerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ internal sealed unsafe partial class RespServerSession : ServerSessionBase
/// </summary>
CustomObjectCommand currentCustomObjectCommand = null;


int respProtocolVersion = 2;
string clientName = null;

public RespServerSession(
INetworkSender networkSender,
StoreWrapper storeWrapper,
Expand Down
Loading

0 comments on commit 5a65fd7

Please sign in to comment.