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.internal.impl.filter; 020 021import javax.inject.Inject; 022import javax.inject.Named; 023import javax.inject.Singleton; 024 025import java.io.BufferedReader; 026import java.io.FileNotFoundException; 027import java.io.IOException; 028import java.io.UncheckedIOException; 029import java.nio.charset.StandardCharsets; 030import java.nio.file.Files; 031import java.nio.file.Path; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.HashMap; 035import java.util.List; 036import java.util.concurrent.ConcurrentHashMap; 037 038import org.eclipse.aether.RepositorySystemSession; 039import org.eclipse.aether.artifact.Artifact; 040import org.eclipse.aether.metadata.Metadata; 041import org.eclipse.aether.repository.RemoteRepository; 042import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter; 043import org.eclipse.aether.spi.connector.layout.RepositoryLayout; 044import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider; 045import org.eclipse.aether.transfer.NoRepositoryLayoutException; 046import org.eclipse.aether.util.ConfigUtils; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049 050import static java.util.Objects.requireNonNull; 051import static java.util.stream.Collectors.toList; 052 053/** 054 * Remote repository filter source filtering on path prefixes. It is backed by a file that lists all allowed path 055 * prefixes from remote repository. Artifact that layout converted path (using remote repository layout) results in 056 * path with no corresponding prefix present in this file is filtered out. 057 * <p> 058 * The file can be authored manually: format is one prefix per line, comments starting with "#" (hash) and empty lines 059 * for structuring are supported, The "/" (slash) character is used as file separator. Some remote repositories and 060 * MRMs publish these kind of files, they can be downloaded from corresponding URLs. 061 * <p> 062 * The prefix file is expected on path "${basedir}/prefixes-${repository.id}.txt". 063 * <p> 064 * The prefixes file is once loaded and cached, so in-flight prefixes file change during component existence are not 065 * noticed. 066 * <p> 067 * Examples of published prefix files: 068 * <ul> 069 * <li>Central: <a href="https://repo.maven.apache.org/maven2/.meta/prefixes.txt">prefixes.txt</a></li> 070 * <li>Apache Releases: 071 * <a href="https://repository.apache.org/content/repositories/releases/.meta/prefixes.txt">prefixes.txt</a></li> 072 * </ul> 073 * 074 * @since 1.9.0 075 */ 076@Singleton 077@Named(PrefixesRemoteRepositoryFilterSource.NAME) 078public final class PrefixesRemoteRepositoryFilterSource extends RemoteRepositoryFilterSourceSupport { 079 public static final String NAME = "prefixes"; 080 081 private static final String CONFIG_PROPS_PREFIX = 082 RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME + "."; 083 084 /** 085 * Is filter enabled? 086 * 087 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 088 * @configurationType {@link java.lang.Boolean} 089 * @configurationDefaultValue false 090 */ 091 public static final String CONFIG_PROP_ENABLED = RemoteRepositoryFilterSourceSupport.CONFIG_PROPS_PREFIX + NAME; 092 093 /** 094 * The basedir where to store filter files. If path is relative, it is resolved from local repository root. 095 * 096 * @configurationSource {@link RepositorySystemSession#getConfigProperties()} 097 * @configurationType {@link java.lang.String} 098 * @configurationDefaultValue {@link #LOCAL_REPO_PREFIX_DIR} 099 */ 100 public static final String CONFIG_PROP_BASEDIR = CONFIG_PROPS_PREFIX + "basedir"; 101 102 public static final String LOCAL_REPO_PREFIX_DIR = ".remoteRepositoryFilters"; 103 104 static final String PREFIXES_FILE_PREFIX = "prefixes-"; 105 106 static final String PREFIXES_FILE_SUFFIX = ".txt"; 107 108 private static final Logger LOGGER = LoggerFactory.getLogger(PrefixesRemoteRepositoryFilterSource.class); 109 110 private final RepositoryLayoutProvider repositoryLayoutProvider; 111 112 private final ConcurrentHashMap<RemoteRepository, Node> prefixes; 113 114 private final ConcurrentHashMap<RemoteRepository, RepositoryLayout> layouts; 115 116 @Inject 117 public PrefixesRemoteRepositoryFilterSource(RepositoryLayoutProvider repositoryLayoutProvider) { 118 this.repositoryLayoutProvider = requireNonNull(repositoryLayoutProvider); 119 this.prefixes = new ConcurrentHashMap<>(); 120 this.layouts = new ConcurrentHashMap<>(); 121 } 122 123 @Override 124 protected boolean isEnabled(RepositorySystemSession session) { 125 return ConfigUtils.getBoolean(session, false, CONFIG_PROP_ENABLED); 126 } 127 128 @Override 129 public RemoteRepositoryFilter getRemoteRepositoryFilter(RepositorySystemSession session) { 130 if (isEnabled(session)) { 131 return new PrefixesFilter(session, getBasedir(session, LOCAL_REPO_PREFIX_DIR, CONFIG_PROP_BASEDIR, false)); 132 } 133 return null; 134 } 135 136 /** 137 * Caches layout instances for remote repository. In case of unknown layout it returns {@code null}. 138 * 139 * @return the layout instance of {@code null} if layout not supported. 140 */ 141 private RepositoryLayout cacheLayout(RepositorySystemSession session, RemoteRepository remoteRepository) { 142 return layouts.computeIfAbsent(remoteRepository, r -> { 143 try { 144 return repositoryLayoutProvider.newRepositoryLayout(session, remoteRepository); 145 } catch (NoRepositoryLayoutException e) { 146 return null; 147 } 148 }); 149 } 150 151 /** 152 * Caches prefixes instances for remote repository. 153 */ 154 private Node cacheNode(Path basedir, RemoteRepository remoteRepository) { 155 return prefixes.computeIfAbsent(remoteRepository, r -> loadRepositoryPrefixes(basedir, remoteRepository)); 156 } 157 158 /** 159 * Loads prefixes file and preprocesses it into {@link Node} instance. 160 */ 161 private Node loadRepositoryPrefixes(Path baseDir, RemoteRepository remoteRepository) { 162 Path filePath = baseDir.resolve(PREFIXES_FILE_PREFIX + remoteRepository.getId() + PREFIXES_FILE_SUFFIX); 163 if (Files.isReadable(filePath)) { 164 try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { 165 LOGGER.debug( 166 "Loading prefixes for remote repository {} from file '{}'", remoteRepository.getId(), filePath); 167 Node root = new Node(""); 168 String prefix; 169 int lines = 0; 170 while ((prefix = reader.readLine()) != null) { 171 if (!prefix.startsWith("#") && !prefix.trim().isEmpty()) { 172 lines++; 173 Node currentNode = root; 174 for (String element : elementsOf(prefix)) { 175 currentNode = currentNode.addSibling(element); 176 } 177 } 178 } 179 LOGGER.info("Loaded {} prefixes for remote repository {}", lines, remoteRepository.getId()); 180 return root; 181 } catch (FileNotFoundException e) { 182 // strange: we tested for it above, still, we should not fail 183 } catch (IOException e) { 184 throw new UncheckedIOException(e); 185 } 186 } 187 LOGGER.debug("Prefix file for remote repository {} not found at '{}'", remoteRepository, filePath); 188 return NOT_PRESENT_NODE; 189 } 190 191 private class PrefixesFilter implements RemoteRepositoryFilter { 192 private final RepositorySystemSession session; 193 194 private final Path basedir; 195 196 private PrefixesFilter(RepositorySystemSession session, Path basedir) { 197 this.session = session; 198 this.basedir = basedir; 199 } 200 201 @Override 202 public Result acceptArtifact(RemoteRepository remoteRepository, Artifact artifact) { 203 RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); 204 if (repositoryLayout == null) { 205 return new SimpleResult(true, "Unsupported layout: " + remoteRepository); 206 } 207 return acceptPrefix( 208 remoteRepository, 209 repositoryLayout.getLocation(artifact, false).getPath()); 210 } 211 212 @Override 213 public Result acceptMetadata(RemoteRepository remoteRepository, Metadata metadata) { 214 RepositoryLayout repositoryLayout = cacheLayout(session, remoteRepository); 215 if (repositoryLayout == null) { 216 return new SimpleResult(true, "Unsupported layout: " + remoteRepository); 217 } 218 return acceptPrefix( 219 remoteRepository, 220 repositoryLayout.getLocation(metadata, false).getPath()); 221 } 222 223 private Result acceptPrefix(RemoteRepository remoteRepository, String path) { 224 Node root = cacheNode(basedir, remoteRepository); 225 if (NOT_PRESENT_NODE == root) { 226 return NOT_PRESENT_RESULT; 227 } 228 List<String> prefix = new ArrayList<>(); 229 final List<String> pathElements = elementsOf(path); 230 Node currentNode = root; 231 for (String pathElement : pathElements) { 232 prefix.add(pathElement); 233 currentNode = currentNode.getSibling(pathElement); 234 if (currentNode == null || currentNode.isLeaf()) { 235 break; 236 } 237 } 238 if (currentNode != null && currentNode.isLeaf()) { 239 return new SimpleResult( 240 true, "Prefix " + String.join("/", prefix) + " allowed from " + remoteRepository); 241 } else { 242 return new SimpleResult( 243 false, "Prefix " + String.join("/", prefix) + " NOT allowed from " + remoteRepository); 244 } 245 } 246 } 247 248 private static final Node NOT_PRESENT_NODE = new Node("not-present-node"); 249 250 private static final RemoteRepositoryFilter.Result NOT_PRESENT_RESULT = 251 new SimpleResult(true, "Prefix file not present"); 252 253 private static class Node { 254 private final String name; 255 256 private final HashMap<String, Node> siblings; 257 258 private Node(String name) { 259 this.name = name; 260 this.siblings = new HashMap<>(); 261 } 262 263 public String getName() { 264 return name; 265 } 266 267 public boolean isLeaf() { 268 return siblings.isEmpty(); 269 } 270 271 public Node addSibling(String name) { 272 Node sibling = siblings.get(name); 273 if (sibling == null) { 274 sibling = new Node(name); 275 siblings.put(name, sibling); 276 } 277 return sibling; 278 } 279 280 public Node getSibling(String name) { 281 return siblings.get(name); 282 } 283 } 284 285 private static List<String> elementsOf(final String path) { 286 return Arrays.stream(path.split("/")) 287 .filter(e -> e != null && !e.isEmpty()) 288 .collect(toList()); 289 } 290}