View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.named.redisson;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.URI;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.util.Locale;
28  
29  import org.eclipse.aether.named.support.NamedLockFactorySupport;
30  import org.redisson.Redisson;
31  import org.redisson.api.RedissonClient;
32  import org.redisson.config.Config;
33  
34  /**
35   * Support class for factories using {@link RedissonClient}.
36   */
37  public abstract class RedissonNamedLockFactorySupport extends NamedLockFactorySupport {
38      protected static final String NAME_PREFIX = "maven:resolver:";
39  
40      private static final String DEFAULT_CONFIG_FILE_NAME = "maven-resolver-redisson.yaml";
41  
42      private static final String DEFAULT_CLIENT_NAME = "maven-resolver";
43  
44      /**
45       * Path to a Redisson configuration file in YAML format. Read official documentation for details.
46       *
47       * @since 1.7.0
48       * @configurationSource {@link System#getProperty(String, String)}
49       * @configurationType {@link java.lang.String}
50       */
51      public static final String SYSTEM_PROP_CONFIG_FILE = "aether.syncContext.named.redisson.configFile";
52  
53      /**
54       * Address of the Redis instance. Optional.
55       *
56       * @since 2.0.0
57       * @configurationSource {@link System#getProperty(String, String)}
58       * @configurationType {@link java.lang.String}
59       * @configurationDefaultValue {@link #DEFAULT_REDIS_ADDRESS}
60       */
61      public static final String SYSTEM_PROP_REDIS_ADDRESS = "aether.syncContext.named.redisson.address";
62  
63      public static final String DEFAULT_REDIS_ADDRESS = "redis://localhost:6379";
64  
65      /**
66       * Whether a plaintext {@code redis://} address pointing at a non-loopback host is allowed. Such a connection is
67       * unencrypted and unauthenticated at the transport, so a tampered, spoofed, or on-path-modified Redis can grant
68       * conflicting locks and corrupt the shared local repository the locks protect. Disabled by default; prefer a
69       * {@code rediss://} (TLS) address, or a Redisson configuration file with authentication, for anything
70       * cross-host.
71       *
72       * @since 2.0.23
73       * @configurationSource {@link System#getProperty(String, String)}
74       * @configurationType {@link java.lang.Boolean}
75       * @configurationDefaultValue false
76       */
77      public static final String SYSTEM_PROP_ALLOW_INSECURE_ADDRESS =
78              "aether.syncContext.named.redisson.allowInsecureAddress";
79  
80      protected final RedissonClient redissonClient;
81  
82      public RedissonNamedLockFactorySupport() {
83          this.redissonClient = createRedissonClient();
84      }
85  
86      @Override
87      protected void doShutdown() {
88          logger.trace("Shutting down Redisson client with id '{}'", redissonClient.getId());
89          redissonClient.shutdown();
90      }
91  
92      private RedissonClient createRedissonClient() {
93          Path configFilePath = null;
94  
95          String configFile = System.getProperty(SYSTEM_PROP_CONFIG_FILE);
96          if (configFile != null && !configFile.isEmpty()) {
97              configFilePath = Paths.get(configFile);
98              if (Files.notExists(configFilePath)) {
99                  throw new IllegalArgumentException(
100                         "The specified Redisson config file does not exist: " + configFilePath);
101             }
102         }
103 
104         if (configFilePath == null) {
105             String mavenConf = System.getProperty("maven.conf");
106             if (mavenConf != null && !mavenConf.isEmpty()) {
107                 configFilePath = Paths.get(mavenConf, DEFAULT_CONFIG_FILE_NAME);
108                 if (Files.notExists(configFilePath)) {
109                     configFilePath = null;
110                 }
111             }
112         }
113 
114         Config config;
115 
116         if (configFilePath != null) {
117             logger.trace("Reading Redisson config file from '{}'", configFilePath);
118             try (InputStream is = Files.newInputStream(configFilePath)) {
119                 config = Config.fromYAML(is);
120             } catch (IOException e) {
121                 throw new IllegalStateException("Failed to read Redisson config file: " + configFilePath, e);
122             }
123         } else {
124             config = new Config();
125             String defaultRedisAddress = System.getProperty(SYSTEM_PROP_REDIS_ADDRESS, DEFAULT_REDIS_ADDRESS);
126             if (isInsecureRemoteAddress(defaultRedisAddress)) {
127                 if (Boolean.getBoolean(SYSTEM_PROP_ALLOW_INSECURE_ADDRESS)) {
128                     logger.warn(
129                             "Using plaintext Redis address '{}' for lock state guarding local repository writes;"
130                                     + " the connection is unencrypted and unauthenticated at the transport, so the"
131                                     + " endpoint and the network path to it must be trusted and isolated",
132                             defaultRedisAddress);
133                 } else {
134                     throw new IllegalStateException("Refusing plaintext non-loopback Redis address '"
135                             + defaultRedisAddress + "': lock answers from a tampered or spoofed Redis can void"
136                             + " mutual exclusion and corrupt the shared local repository. Use a 'rediss://' (TLS)"
137                             + " address, or a Redisson configuration file ('" + SYSTEM_PROP_CONFIG_FILE
138                             + "') with authentication, or explicitly opt in with -D"
139                             + SYSTEM_PROP_ALLOW_INSECURE_ADDRESS + "=true");
140                 }
141             }
142             config.useSingleServer().setAddress(defaultRedisAddress).setClientName(DEFAULT_CLIENT_NAME);
143         }
144 
145         RedissonClient redissonClient = Redisson.create(config);
146         logger.trace("Created Redisson client with id '{}'", redissonClient.getId());
147 
148         return redissonClient;
149     }
150 
151     /**
152      * Returns {@code true} if the given address is a plaintext {@code redis://} address pointing at a non-loopback
153      * host. TLS ({@code rediss://}) addresses and loopback addresses are acceptable defaults; anything else is
154      * insecure. Unparseable addresses are treated as insecure (fail closed).
155      */
156     static boolean isInsecureRemoteAddress(String address) {
157         if (address.toLowerCase(Locale.ROOT).startsWith("rediss://")) {
158             return false; // TLS protects the channel
159         }
160         String host;
161         try {
162             host = URI.create(address).getHost();
163         } catch (IllegalArgumentException e) {
164             return true;
165         }
166         if (host == null) {
167             return true;
168         }
169         return !("localhost".equalsIgnoreCase(host)
170                 || host.startsWith("127.")
171                 || "::1".equals(host)
172                 || "[::1]".equals(host));
173     }
174 }