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