001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.named.redisson; 020 021import java.io.IOException; 022import java.io.InputStream; 023import java.net.URI; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.nio.file.Paths; 027import java.util.Locale; 028 029import org.eclipse.aether.named.support.NamedLockFactorySupport; 030import org.redisson.Redisson; 031import org.redisson.api.RedissonClient; 032import org.redisson.config.Config; 033 034/** 035 * Support class for factories using {@link RedissonClient}. 036 */ 037public abstract class RedissonNamedLockFactorySupport extends NamedLockFactorySupport { 038 protected static final String NAME_PREFIX = "maven:resolver:"; 039 040 private static final String DEFAULT_CONFIG_FILE_NAME = "maven-resolver-redisson.yaml"; 041 042 private static final String DEFAULT_CLIENT_NAME = "maven-resolver"; 043 044 /** 045 * Path to a Redisson configuration file in YAML format. Read official documentation for details. 046 * 047 * @since 1.7.0 048 * @configurationSource {@link System#getProperty(String, String)} 049 * @configurationType {@link java.lang.String} 050 */ 051 public static final String SYSTEM_PROP_CONFIG_FILE = "aether.syncContext.named.redisson.configFile"; 052 053 /** 054 * Address of the Redis instance. Optional. 055 * 056 * @since 2.0.0 057 * @configurationSource {@link System#getProperty(String, String)} 058 * @configurationType {@link java.lang.String} 059 * @configurationDefaultValue {@link #DEFAULT_REDIS_ADDRESS} 060 */ 061 public static final String SYSTEM_PROP_REDIS_ADDRESS = "aether.syncContext.named.redisson.address"; 062 063 public static final String DEFAULT_REDIS_ADDRESS = "redis://localhost:6379"; 064 065 /** 066 * Whether a plaintext {@code redis://} address pointing at a non-loopback host is allowed. Such a connection is 067 * unencrypted and unauthenticated at the transport, so a tampered, spoofed, or on-path-modified Redis can grant 068 * conflicting locks and corrupt the shared local repository the locks protect. Disabled by default; prefer a 069 * {@code rediss://} (TLS) address, or a Redisson configuration file with authentication, for anything 070 * cross-host. 071 * 072 * @since 2.0.23 073 * @configurationSource {@link System#getProperty(String, String)} 074 * @configurationType {@link java.lang.Boolean} 075 * @configurationDefaultValue false 076 */ 077 public static final String SYSTEM_PROP_ALLOW_INSECURE_ADDRESS = 078 "aether.syncContext.named.redisson.allowInsecureAddress"; 079 080 protected final RedissonClient redissonClient; 081 082 public RedissonNamedLockFactorySupport() { 083 this.redissonClient = createRedissonClient(); 084 } 085 086 @Override 087 protected void doShutdown() { 088 logger.trace("Shutting down Redisson client with id '{}'", redissonClient.getId()); 089 redissonClient.shutdown(); 090 } 091 092 private RedissonClient createRedissonClient() { 093 Path configFilePath = null; 094 095 String configFile = System.getProperty(SYSTEM_PROP_CONFIG_FILE); 096 if (configFile != null && !configFile.isEmpty()) { 097 configFilePath = Paths.get(configFile); 098 if (Files.notExists(configFilePath)) { 099 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}