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