]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
Implement partial support for Redis provisional URI scheme
authorJoel Rosdahl <joel@rosdahl.net>
Sun, 11 Jul 2021 18:32:08 +0000 (20:32 +0200)
committerJoel Rosdahl <joel@rosdahl.net>
Mon, 12 Jul 2021 20:42:42 +0000 (22:42 +0200)
Reference: https://www.iana.org/assignments/uri-schemes/prov/redis

Supported: Username, password, host, port and db number. Not supported:
connection parameters.

This also drops support for Unix sockets.

doc/MANUAL.adoc
src/storage/SecondaryStorage.hpp
src/storage/secondary/RedisStorage.cpp
src/storage/secondary/RedisStorage.hpp

index 6d3e5316e8655809ffa94694720abfc541a87294..6b66f9aa620fc612e374b18783c93d5ea4250e96 100644 (file)
@@ -956,25 +956,24 @@ Known issues and limitations:
 
 === Redis storage backend
 
-URL format: `redis://HOST[:PORT]` or `redis://UNIX_SOCKET_PATH`
+URL format: `redis://[[USERNAME:]PASSWORD@]HOST[:PORT][/DBNUMBER]`
 
 This backend stores data in a https://redis.io[Redis] (or Redis-compatible)
 server. There are implementations for both memory-based and disk-based storage.
+*PORT* defaults to *6379* and *DBNUMBER* defaults to *0*.
 
 Note that ccache will not perform any cleanup of the Redis storage, but you can
 https://redis.io/topics/lru-cache[configure LRU eviction].
 
 Examples:
 
-* `redis://localhost:6379|connect-timeout=50`
-* `redis:///var/run/redis/redis-server.sock`
+* `redis://localhost`
+* `redis://p4ssw0rd@cache.example.com:6379/0|connect-timeout=50`
 
 Optional attributes:
 
 * *connect-timeout*: Timeout (in ms) for network connection. The default is 100.
 * *operation-timeout*: Timeout (in ms) for Redis commands. The default is 10000.
-* *username*: Username for the Redis AUTH command (requires Redis 6 or newer).
-* *password*: Password for the Redis AUTH command.
 
 == Cache size management
 
index 611f78783e4d17fa0aabae7e6e750f8c817d42e7..6fec9c911d021c25e21018625747a3832ad82a73 100644 (file)
@@ -27,6 +27,8 @@ class Digest;
 
 namespace storage {
 
+constexpr auto k_masked_password = "********";
+
 // This class defines the API that a secondary storage backend must implement.
 class SecondaryStorage
 {
index 6e99daf446917fa9eaa1be46ced1ed4fd03d7fa8..a05dc1b4609f747fd67b24740c172a202184fec8 100644 (file)
@@ -21,6 +21,7 @@
 #include <Digest.hpp>
 #include <Logging.hpp>
 #include <fmtmacros.hpp>
+#include <util/string_utils.hpp>
 
 #include <hiredis/hiredis.h>
 
@@ -32,7 +33,7 @@ namespace secondary {
 
 const uint64_t DEFAULT_CONNECT_TIMEOUT_MS = 100;
 const uint64_t DEFAULT_OPERATION_TIMEOUT_MS = 10000;
-const int DEFAULT_PORT = 6379;
+const uint32_t DEFAULT_PORT = 6379;
 
 using RedisReply = std::unique_ptr<redisReply, decltype(&freeReplyObject)>;
 
@@ -68,14 +69,20 @@ parse_timeout_attribute(const AttributeMap& attributes,
   }
 }
 
-static nonstd::optional<std::string>
-parse_string_attribute(const AttributeMap& attributes, const std::string& name)
+static std::pair<nonstd::optional<std::string>, nonstd::optional<std::string>>
+split_user_info(const std::string& user_info)
 {
-  const auto it = attributes.find(name);
-  if (it == attributes.end()) {
-    return nonstd::nullopt;
+  const auto pair = util::split_once(user_info, ':');
+  if (pair.first.empty()) {
+    // redis://HOST
+    return {nonstd::nullopt, nonstd::nullopt};
+  } else if (pair.second) {
+    // redis://USERNAME:PASSWORD@HOST
+    return {to_string(*pair.second), to_string(pair.first)};
+  } else {
+    // redis://PASSWORD@HOST
+    return {to_string(pair.first), nonstd::nullopt};
   }
-  return it->second;
 }
 
 RedisStorage::RedisStorage(const Url& url, const AttributeMap& attributes)
@@ -86,8 +93,6 @@ RedisStorage::RedisStorage(const Url& url, const AttributeMap& attributes)
       attributes, "connect-timeout", DEFAULT_CONNECT_TIMEOUT_MS)),
     m_operation_timeout(parse_timeout_attribute(
       attributes, "operation-timeout", DEFAULT_OPERATION_TIMEOUT_MS)),
-    m_username(parse_string_attribute(attributes, "username")),
-    m_password(parse_string_attribute(attributes, "password")),
     m_connected(false),
     m_invalid(false)
 {
@@ -123,28 +128,26 @@ RedisStorage::connect()
   }
 
   ASSERT(m_url.scheme() == "redis");
-  const auto& host = m_url.host();
-  const auto& port = m_url.port();
-  const auto& sock = m_url.path();
+  const std::string host = m_url.host().empty() ? "localhost" : m_url.host();
+  const uint32_t port =
+    m_url.port().empty() ? DEFAULT_PORT
+                         : Util::parse_unsigned(m_url.port(), 1, 65535, "port");
+  ASSERT(m_url.path().empty() || m_url.path()[0] == '/');
+  const uint32_t db_number =
+    m_url.path().empty()
+      ? 0
+      : Util::parse_unsigned(m_url.path().substr(1),
+                             0,
+                             std::numeric_limits<uint32_t>::max(),
+                             "db number");
+
   const auto connect_timeout = milliseconds_to_timeval(m_connect_timeout);
-  if (!host.empty()) {
-    const int p = port.empty() ? DEFAULT_PORT
-                               : Util::parse_unsigned(port, 1, 65535, "port");
-    LOG("Redis connecting to {}:{} (timeout {} ms)",
-        host.c_str(),
-        p,
-        m_connect_timeout);
-    m_context = redisConnectWithTimeout(host.c_str(), p, connect_timeout);
-  } else if (!sock.empty()) {
-    LOG("Redis connecting to {} (timeout {} ms)",
-        sock.c_str(),
-        m_connect_timeout);
-    m_context = redisConnectUnixWithTimeout(sock.c_str(), connect_timeout);
-  } else {
-    LOG("Invalid Redis URL: {}", m_url.str());
-    m_invalid = true;
-    return REDIS_ERR;
-  }
+
+  LOG("Redis connecting to {}:{} (timeout {} ms)",
+      host.c_str(),
+      port,
+      m_connect_timeout);
+  m_context = redisConnectWithTimeout(host.c_str(), port, connect_timeout);
 
   if (!m_context) {
     LOG_RAW("Redis connection error (NULL context)");
@@ -154,48 +157,55 @@ RedisStorage::connect()
     LOG("Redis connection error: {}", m_context->errstr);
     m_invalid = true;
     return m_context->err;
-  } else {
-    if (m_context->connection_type == REDIS_CONN_TCP) {
-      LOG("Redis connection to {}:{} OK",
-          m_context->tcp.host,
-          m_context->tcp.port);
-    }
-    if (m_context->connection_type == REDIS_CONN_UNIX) {
-      LOG("Redis connection to {} OK", m_context->unix_sock.path);
-    }
-    m_connected = true;
+  }
 
-    if (redisSetTimeout(m_context, milliseconds_to_timeval(m_operation_timeout))
-        != REDIS_OK) {
-      LOG_RAW("Failed to set operation timeout");
-    }
+  LOG("Redis connection to {}:{} OK", m_context->tcp.host, m_context->tcp.port);
+  m_connected = true;
 
-    return auth();
+  if (redisSetTimeout(m_context, milliseconds_to_timeval(m_operation_timeout))
+      != REDIS_OK) {
+    LOG_RAW("Failed to set operation timeout");
   }
+
+  if (db_number != 0) {
+    LOG("Redis SELECT {}", db_number);
+    const auto reply = redis_command(m_context, "SELECT %d", db_number);
+    if (!reply) {
+      LOG_RAW("Redis SELECT failed (NULL)");
+      m_invalid = true;
+      return REDIS_ERR;
+    } else if (reply->type == REDIS_REPLY_ERROR) {
+      LOG("Redis SELECT error: {}", reply->str);
+      m_invalid = true;
+      return REDIS_ERR;
+    }
+  }
+
+  return auth();
 }
 
 int
 RedisStorage::auth()
 {
-  if (m_password) {
-    bool log_password = false;
-    std::string username = m_username ? *m_username : "default";
-    std::string password = log_password ? *m_password : "*******";
-    LOG("Redis AUTH {} {}", username, password);
-
+  const auto password_username_pair = split_user_info(m_url.user_info());
+  const auto& password = password_username_pair.first;
+  if (password) {
     RedisReply reply(nullptr, freeReplyObject);
-    if (m_username) {
+    const auto& username = password_username_pair.second;
+    if (username) {
+      LOG("Redis AUTH {} {}", *username, storage::k_masked_password);
       reply = redis_command(
-        m_context, "AUTH %s %s", m_username->c_str(), m_password->c_str());
+        m_context, "AUTH %s %s", username->c_str(), password->c_str());
     } else {
-      reply = redis_command(m_context, "AUTH %s", m_password->c_str());
+      LOG("Redis AUTH {}", storage::k_masked_password);
+      reply = redis_command(m_context, "AUTH %s", password->c_str());
     }
     if (!reply) {
-      LOG_RAW("Failed to authenticate to Redis (NULL)");
+      LOG_RAW("Redis AUTH failed (NULL)");
       m_invalid = true;
       return REDIS_ERR;
     } else if (reply->type == REDIS_REPLY_ERROR) {
-      LOG("Failed to authenticate to Redis: {}", reply->str);
+      LOG("Redis AUTH error: {}", reply->str);
       m_invalid = true;
       return REDIS_ERR;
     }
index 04d27de6129aac41cf7aab48861256d49f35df55..3e2ed45384faf2566ca28ac6063d0ac326072a50 100644 (file)
@@ -47,8 +47,6 @@ private:
   redisContext* m_context;
   const uint64_t m_connect_timeout;
   const uint64_t m_operation_timeout;
-  const nonstd::optional<std::string> m_username;
-  const nonstd::optional<std::string> m_password;
   bool m_connected;
   bool m_invalid;