]> source.dussan.org Git - archiva.git/blob
e4e60fe73145a4e0df3110f7bf5311452436e3a2
[archiva.git] /
1 package org.apache.archiva.proxy;
2
3 /*
4  * Licensed to the Apache Software Foundation (ASF) under one
5  * or more contributor license agreements.  See the NOTICE file
6  * distributed with this work for additional information
7  * regarding copyright ownership.  The ASF licenses this file
8  * to you under the Apache License, Version 2.0 (the
9  * "License"); you may not use this file except in compliance
10  * with the License.  You may obtain a copy of the License at
11  *
12  *  http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  * KIND, either express or implied.  See the License for the
18  * specific language governing permissions and limitations
19  * under the License.
20  */
21
22 import org.apache.archiva.admin.model.RepositoryAdminException;
23 import org.apache.archiva.admin.model.beans.NetworkProxy;
24 import org.apache.archiva.admin.model.beans.ProxyConnectorRuleType;
25 import org.apache.archiva.admin.model.beans.RemoteRepository;
26 import org.apache.archiva.admin.model.networkproxy.NetworkProxyAdmin;
27 import org.apache.archiva.common.filelock.FileLockException;
28 import org.apache.archiva.common.filelock.FileLockManager;
29 import org.apache.archiva.common.filelock.FileLockTimeoutException;
30 import org.apache.archiva.common.filelock.Lock;
31 import org.apache.archiva.configuration.*;
32 import org.apache.archiva.model.ArtifactReference;
33 import org.apache.archiva.model.Keys;
34 import org.apache.archiva.model.RepositoryURL;
35 import org.apache.archiva.policies.*;
36 import org.apache.archiva.policies.urlcache.UrlFailureCache;
37 import org.apache.archiva.proxy.common.WagonFactory;
38 import org.apache.archiva.proxy.common.WagonFactoryException;
39 import org.apache.archiva.proxy.common.WagonFactoryRequest;
40 import org.apache.archiva.proxy.model.ProxyConnector;
41 import org.apache.archiva.proxy.model.ProxyFetchResult;
42 import org.apache.archiva.proxy.model.RepositoryProxyConnectors;
43 import org.apache.archiva.redback.components.registry.Registry;
44 import org.apache.archiva.redback.components.registry.RegistryListener;
45 import org.apache.archiva.redback.components.taskqueue.TaskQueueException;
46 import org.apache.archiva.repository.*;
47 import org.apache.archiva.repository.metadata.MetadataTools;
48 import org.apache.archiva.repository.metadata.RepositoryMetadataException;
49 import org.apache.archiva.scheduler.ArchivaTaskScheduler;
50 import org.apache.archiva.scheduler.repository.model.RepositoryTask;
51 import org.apache.commons.collections.CollectionUtils;
52 import org.apache.commons.io.FilenameUtils;
53 import org.apache.commons.lang.StringUtils;
54 import org.apache.commons.lang.SystemUtils;
55 import org.apache.maven.wagon.ConnectionException;
56 import org.apache.maven.wagon.ResourceDoesNotExistException;
57 import org.apache.maven.wagon.Wagon;
58 import org.apache.maven.wagon.WagonException;
59 import org.apache.maven.wagon.authentication.AuthenticationException;
60 import org.apache.maven.wagon.authentication.AuthenticationInfo;
61 import org.apache.maven.wagon.proxy.ProxyInfo;
62 import org.apache.maven.wagon.repository.Repository;
63 import org.apache.tools.ant.types.selectors.SelectorUtils;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66 import org.slf4j.MarkerFactory;
67 import org.springframework.stereotype.Service;
68
69 import javax.annotation.PostConstruct;
70 import javax.inject.Inject;
71 import javax.inject.Named;
72 import java.io.IOException;
73 import java.nio.file.Files;
74 import java.nio.file.Path;
75 import java.nio.file.Paths;
76 import java.util.*;
77 import java.util.Map.Entry;
78 import java.util.concurrent.ConcurrentHashMap;
79 import java.util.concurrent.ConcurrentMap;
80 import java.util.concurrent.TimeUnit;
81
82 /**
83  * DefaultRepositoryProxyConnectors
84  * TODO exception handling needs work - "not modified" is not really an exceptional case, and it has more layers than
85  * your average brown onion
86  */
87 @Service("repositoryProxyConnectors#default")
88 public class DefaultRepositoryProxyConnectors
89     implements RepositoryProxyConnectors, RegistryListener
90 {
91     private Logger log = LoggerFactory.getLogger( DefaultRepositoryProxyConnectors.class );
92
93     @Inject
94     @Named(value = "archivaConfiguration#default")
95     private ArchivaConfiguration archivaConfiguration;
96
97     @Inject
98     @Named(value = "repositoryContentFactory#default")
99     private RepositoryContentFactory repositoryFactory;
100
101     @Inject
102     @Named(value = "metadataTools#default")
103     private MetadataTools metadataTools;
104
105     @Inject
106     private Map<String, PreDownloadPolicy> preDownloadPolicies;
107
108     @Inject
109     private Map<String, PostDownloadPolicy> postDownloadPolicies;
110
111     @Inject
112     private Map<String, DownloadErrorPolicy> downloadErrorPolicies;
113
114     @Inject
115     private UrlFailureCache urlFailureCache;
116
117     private ConcurrentMap<String, List<ProxyConnector>> proxyConnectorMap = new ConcurrentHashMap<>();
118
119     private ConcurrentMap<String, ProxyInfo> networkProxyMap = new ConcurrentHashMap<>();
120
121     @Inject
122     private WagonFactory wagonFactory;
123
124     @Inject
125     @Named(value = "archivaTaskScheduler#repository")
126     private ArchivaTaskScheduler scheduler;
127
128     @Inject
129     private NetworkProxyAdmin networkProxyAdmin;
130
131     @Inject
132     @Named(value = "fileLockManager#default")
133     private FileLockManager fileLockManager;
134
135     @PostConstruct
136     public void initialize()
137     {
138         initConnectorsAndNetworkProxies();
139         archivaConfiguration.addChangeListener( this );
140
141     }
142
143     @SuppressWarnings("unchecked")
144     private void initConnectorsAndNetworkProxies()
145     {
146
147         ProxyConnectorOrderComparator proxyOrderSorter = new ProxyConnectorOrderComparator();
148         this.proxyConnectorMap.clear();
149
150         Configuration configuration = archivaConfiguration.getConfiguration();
151
152         List<ProxyConnectorRuleConfiguration> allProxyConnectorRuleConfigurations =
153             configuration.getProxyConnectorRuleConfigurations();
154
155         List<ProxyConnectorConfiguration> proxyConfigs = configuration.getProxyConnectors();
156         for ( ProxyConnectorConfiguration proxyConfig : proxyConfigs )
157         {
158             String key = proxyConfig.getSourceRepoId();
159
160             try
161             {
162                 // Create connector object.
163                 ProxyConnector connector = new ProxyConnector();
164
165                 connector.setSourceRepository(
166                     repositoryFactory.getManagedRepositoryContent( proxyConfig.getSourceRepoId() ) );
167                 connector.setTargetRepository(
168                     repositoryFactory.getRemoteRepositoryContent( proxyConfig.getTargetRepoId() ) );
169
170                 connector.setProxyId( proxyConfig.getProxyId() );
171                 connector.setPolicies( proxyConfig.getPolicies() );
172                 connector.setOrder( proxyConfig.getOrder() );
173                 connector.setDisabled( proxyConfig.isDisabled() );
174
175                 // Copy any blacklist patterns.
176                 List<String> blacklist = new ArrayList<>( 0 );
177                 if ( CollectionUtils.isNotEmpty( proxyConfig.getBlackListPatterns() ) )
178                 {
179                     blacklist.addAll( proxyConfig.getBlackListPatterns() );
180                 }
181                 connector.setBlacklist( blacklist );
182
183                 // Copy any whitelist patterns.
184                 List<String> whitelist = new ArrayList<>( 0 );
185                 if ( CollectionUtils.isNotEmpty( proxyConfig.getWhiteListPatterns() ) )
186                 {
187                     whitelist.addAll( proxyConfig.getWhiteListPatterns() );
188                 }
189                 connector.setWhitelist( whitelist );
190
191                 List<ProxyConnectorRuleConfiguration> proxyConnectorRuleConfigurations =
192                     findProxyConnectorRules( connector.getSourceRepository().getId(),
193                                              connector.getTargetRepository().getId(),
194                                              allProxyConnectorRuleConfigurations );
195
196                 if ( !proxyConnectorRuleConfigurations.isEmpty() )
197                 {
198                     for ( ProxyConnectorRuleConfiguration proxyConnectorRuleConfiguration : proxyConnectorRuleConfigurations )
199                     {
200                         if ( StringUtils.equals( proxyConnectorRuleConfiguration.getRuleType(),
201                                                  ProxyConnectorRuleType.BLACK_LIST.getRuleType() ) )
202                         {
203                             connector.getBlacklist().add( proxyConnectorRuleConfiguration.getPattern() );
204                         }
205
206                         if ( StringUtils.equals( proxyConnectorRuleConfiguration.getRuleType(),
207                                                  ProxyConnectorRuleType.WHITE_LIST.getRuleType() ) )
208                         {
209                             connector.getWhitelist().add( proxyConnectorRuleConfiguration.getPattern() );
210                         }
211                     }
212                 }
213
214                 // Get other connectors
215                 List<ProxyConnector> connectors = this.proxyConnectorMap.get( key );
216                 if ( connectors == null )
217                 {
218                     // Create if we are the first.
219                     connectors = new ArrayList<>( 1 );
220                 }
221
222                 // Add the connector.
223                 connectors.add( connector );
224
225                 // Ensure the list is sorted.
226                 Collections.sort( connectors, proxyOrderSorter );
227
228                 // Set the key to the list of connectors.
229                 this.proxyConnectorMap.put( key, connectors );
230             }
231             catch ( RepositoryNotFoundException e )
232             {
233                 log.warn( "Unable to use proxy connector: {}", e.getMessage(), e );
234             }
235             catch ( RepositoryException e )
236             {
237                 log.warn( "Unable to use proxy connector: {}", e.getMessage(), e );
238             }
239
240
241         }
242
243         this.networkProxyMap.clear();
244
245         List<NetworkProxyConfiguration> networkProxies = archivaConfiguration.getConfiguration().getNetworkProxies();
246         for ( NetworkProxyConfiguration networkProxyConfig : networkProxies )
247         {
248             String key = networkProxyConfig.getId();
249
250             ProxyInfo proxy = new ProxyInfo();
251
252             proxy.setType( networkProxyConfig.getProtocol() );
253             proxy.setHost( networkProxyConfig.getHost() );
254             proxy.setPort( networkProxyConfig.getPort() );
255             proxy.setUserName( networkProxyConfig.getUsername() );
256             proxy.setPassword( networkProxyConfig.getPassword() );
257
258             this.networkProxyMap.put( key, proxy );
259         }
260
261     }
262
263     private List<ProxyConnectorRuleConfiguration> findProxyConnectorRules( String sourceRepository,
264                                                                            String targetRepository,
265                                                                            List<ProxyConnectorRuleConfiguration> all )
266     {
267         List<ProxyConnectorRuleConfiguration> proxyConnectorRuleConfigurations = new ArrayList<>();
268
269         for ( ProxyConnectorRuleConfiguration proxyConnectorRuleConfiguration : all )
270         {
271             for ( ProxyConnectorConfiguration proxyConnector : proxyConnectorRuleConfiguration.getProxyConnectors() )
272             {
273                 if ( StringUtils.equals( sourceRepository, proxyConnector.getSourceRepoId() ) && StringUtils.equals(
274                     targetRepository, proxyConnector.getTargetRepoId() ) )
275                 {
276                     proxyConnectorRuleConfigurations.add( proxyConnectorRuleConfiguration );
277                 }
278             }
279         }
280
281         return proxyConnectorRuleConfigurations;
282     }
283
284     @Override
285     public Path fetchFromProxies( ManagedRepositoryContent repository, ArtifactReference artifact )
286         throws ProxyDownloadException
287     {
288         Path localFile = toLocalFile( repository, artifact );
289
290         Properties requestProperties = new Properties();
291         requestProperties.setProperty( "filetype", "artifact" );
292         requestProperties.setProperty( "version", artifact.getVersion() );
293         requestProperties.setProperty( "managedRepositoryId", repository.getId() );
294
295         List<ProxyConnector> connectors = getProxyConnectors( repository );
296         Map<String, Exception> previousExceptions = new LinkedHashMap<>();
297         for ( ProxyConnector connector : connectors )
298         {
299             if ( connector.isDisabled() )
300             {
301                 continue;
302             }
303
304             RemoteRepositoryContent targetRepository = connector.getTargetRepository();
305             requestProperties.setProperty( "remoteRepositoryId", targetRepository.getId() );
306
307             String targetPath = targetRepository.toPath( artifact );
308
309             if ( SystemUtils.IS_OS_WINDOWS )
310             {
311                 // toPath use system PATH_SEPARATOR so on windows url are \ which doesn't work very well :-)
312                 targetPath = FilenameUtils.separatorsToUnix( targetPath );
313             }
314
315             try
316             {
317                 Path downloadedFile =
318                     transferFile( connector, targetRepository, targetPath, repository, localFile, requestProperties,
319                                   true );
320
321                 if ( fileExists( downloadedFile ) )
322                 {
323                     log.debug( "Successfully transferred: {}", downloadedFile.toAbsolutePath() );
324                     return downloadedFile;
325                 }
326             }
327             catch ( NotFoundException e )
328             {
329                 log.debug( "Artifact {} not found on repository \"{}\".", Keys.toKey( artifact ),
330                            targetRepository.getRepository().getId() );
331             }
332             catch ( NotModifiedException e )
333             {
334                 log.debug( "Artifact {} not updated on repository \"{}\".", Keys.toKey( artifact ),
335                            targetRepository.getRepository().getId() );
336             }
337             catch ( ProxyException | RepositoryAdminException e )
338             {
339                 validatePolicies( this.downloadErrorPolicies, connector.getPolicies(), requestProperties, artifact,
340                                   targetRepository, localFile, e, previousExceptions );
341             }
342         }
343
344         if ( !previousExceptions.isEmpty() )
345         {
346             throw new ProxyDownloadException( "Failures occurred downloading from some remote repositories",
347                                               previousExceptions );
348         }
349
350         log.debug( "Exhausted all target repositories, artifact {} not found.", Keys.toKey( artifact ) );
351
352         return null;
353     }
354
355     @Override
356     public Path fetchFromProxies( ManagedRepositoryContent repository, String path )
357     {
358         Path localFile = Paths.get( repository.getRepoRoot(), path );
359
360         // no update policies for these paths
361         if ( Files.exists(localFile) )
362         {
363             return null;
364         }
365
366         Properties requestProperties = new Properties();
367         requestProperties.setProperty( "filetype", "resource" );
368         requestProperties.setProperty( "managedRepositoryId", repository.getId() );
369
370         List<ProxyConnector> connectors = getProxyConnectors( repository );
371         for ( ProxyConnector connector : connectors )
372         {
373             if ( connector.isDisabled() )
374             {
375                 continue;
376             }
377
378             RemoteRepositoryContent targetRepository = connector.getTargetRepository();
379             requestProperties.setProperty( "remoteRepositoryId", targetRepository.getId() );
380
381             String targetPath = path;
382
383             try
384             {
385                 Path downloadedFile =
386                     transferFile( connector, targetRepository, targetPath, repository, localFile, requestProperties,
387                                   false );
388
389                 if ( fileExists( downloadedFile ) )
390                 {
391                     log.debug( "Successfully transferred: {}", downloadedFile.toAbsolutePath() );
392                     return downloadedFile;
393                 }
394             }
395             catch ( NotFoundException e )
396             {
397                 log.debug( "Resource {} not found on repository \"{}\".", path,
398                            targetRepository.getRepository().getId() );
399             }
400             catch ( NotModifiedException e )
401             {
402                 log.debug( "Resource {} not updated on repository \"{}\".", path,
403                            targetRepository.getRepository().getId() );
404             }
405             catch ( ProxyException e )
406             {
407                 log.warn(
408                     "Transfer error from repository {} for resource {}, continuing to next repository. Error message: {}",
409                     targetRepository.getRepository().getId(), path, e.getMessage() );
410                 log.debug( MarkerFactory.getDetachedMarker( "transfer.error" ),
411                            "Transfer error from repository \"{}"
412                                + "\" for resource {}, continuing to next repository. Error message: {}",
413                            targetRepository.getRepository().getId(), path, e.getMessage(), e );
414             }
415             catch ( RepositoryAdminException e )
416             {
417                 log.debug( MarkerFactory.getDetachedMarker( "transfer.error" ),
418                            "Transfer error from repository {} for resource {}, continuing to next repository. Error message: {}",
419                            targetRepository.getRepository().getId(), path, e.getMessage(), e );
420                 log.debug( MarkerFactory.getDetachedMarker( "transfer.error" ), "Full stack trace", e );
421             }
422         }
423
424         log.debug( "Exhausted all target repositories, resource {} not found.", path );
425
426         return null;
427     }
428
429     @Override
430     public ProxyFetchResult fetchMetadataFromProxies( ManagedRepositoryContent repository, String logicalPath )
431     {
432         Path localFile = Paths.get( repository.getRepoRoot(), logicalPath );
433
434         Properties requestProperties = new Properties();
435         requestProperties.setProperty( "filetype", "metadata" );
436         boolean metadataNeedsUpdating = false;
437         long originalTimestamp = getLastModified( localFile );
438
439         List<ProxyConnector> connectors = new ArrayList<>( getProxyConnectors( repository ) );
440         for ( ProxyConnector connector : connectors )
441         {
442             if ( connector.isDisabled() )
443             {
444                 continue;
445             }
446
447             RemoteRepositoryContent targetRepository = connector.getTargetRepository();
448
449             Path localRepoFile = toLocalRepoFile( repository, targetRepository, logicalPath );
450             long originalMetadataTimestamp = getLastModified( localRepoFile );
451
452             try
453             {
454                 transferFile( connector, targetRepository, logicalPath, repository, localRepoFile, requestProperties,
455                               true );
456
457                 if ( hasBeenUpdated( localRepoFile, originalMetadataTimestamp ) )
458                 {
459                     metadataNeedsUpdating = true;
460                 }
461             }
462             catch ( NotFoundException e )
463             {
464
465                 log.debug( "Metadata {} not found on remote repository '{}'.", logicalPath,
466                            targetRepository.getRepository().getId(), e );
467
468             }
469             catch ( NotModifiedException e )
470             {
471
472                 log.debug( "Metadata {} not updated on remote repository '{}'.", logicalPath,
473                            targetRepository.getRepository().getId(), e );
474
475             }
476             catch ( ProxyException | RepositoryAdminException e )
477             {
478                 log.warn(
479                     "Transfer error from repository {} for versioned Metadata {}, continuing to next repository. Error message: {}",
480                     targetRepository.getRepository().getId(), logicalPath, e.getMessage() );
481                 log.debug( "Full stack trace", e );
482             }
483         }
484
485         if ( hasBeenUpdated( localFile, originalTimestamp ) )
486         {
487             metadataNeedsUpdating = true;
488         }
489
490         if ( metadataNeedsUpdating || !Files.exists(localFile))
491         {
492             try
493             {
494                 metadataTools.updateMetadata( repository, logicalPath );
495             }
496             catch ( RepositoryMetadataException e )
497             {
498                 log.warn( "Unable to update metadata {}:{}", localFile.toAbsolutePath(), e.getMessage(), e );
499             }
500
501         }
502
503         if ( fileExists( localFile ) )
504         {
505             return new ProxyFetchResult( localFile, metadataNeedsUpdating );
506         }
507
508         return new ProxyFetchResult( null, false );
509     }
510
511     /**
512      * @param connector
513      * @param remoteRepository
514      * @param tmpMd5
515      * @param tmpSha1
516      * @param tmpResource
517      * @param url
518      * @param remotePath
519      * @param resource
520      * @param workingDirectory
521      * @param repository
522      * @throws ProxyException
523      * @throws NotModifiedException
524      * @throws org.apache.archiva.admin.model.RepositoryAdminException
525      */
526     protected void transferResources( ProxyConnector connector, RemoteRepositoryContent remoteRepository, Path tmpMd5,
527                                       Path tmpSha1, Path tmpResource, String url, String remotePath, Path resource,
528                                       Path workingDirectory, ManagedRepositoryContent repository )
529         throws ProxyException, NotModifiedException, RepositoryAdminException
530     {
531         Wagon wagon = null;
532         try
533         {
534             RepositoryURL repoUrl = remoteRepository.getURL();
535             String protocol = repoUrl.getProtocol();
536             NetworkProxy networkProxy = null;
537             if ( StringUtils.isNotBlank( connector.getProxyId() ) )
538             {
539                 networkProxy = networkProxyAdmin.getNetworkProxy( connector.getProxyId() );
540             }
541             WagonFactoryRequest wagonFactoryRequest = new WagonFactoryRequest( "wagon#" + protocol,
542                                                                                remoteRepository.getRepository().getExtraHeaders() ).networkProxy(
543                 networkProxy );
544             wagon = wagonFactory.getWagon( wagonFactoryRequest );
545             if ( wagon == null )
546             {
547                 throw new ProxyException( "Unsupported target repository protocol: " + protocol );
548             }
549
550             if ( wagon == null )
551             {
552                 throw new ProxyException( "Unsupported target repository protocol: " + protocol );
553             }
554
555             boolean connected = connectToRepository( connector, wagon, remoteRepository );
556             if ( connected )
557             {
558                 transferArtifact( wagon, remoteRepository, remotePath, repository, resource, workingDirectory,
559                                   tmpResource );
560
561                 // TODO: these should be used to validate the download based on the policies, not always downloaded
562                 // to
563                 // save on connections since md5 is rarely used
564                 transferChecksum( wagon, remoteRepository, remotePath, repository, resource, workingDirectory, ".sha1",
565                                   tmpSha1 );
566                 transferChecksum( wagon, remoteRepository, remotePath, repository, resource, workingDirectory, ".md5",
567                                   tmpMd5 );
568             }
569         }
570         catch ( NotFoundException e )
571         {
572             urlFailureCache.cacheFailure( url );
573             throw e;
574         }
575         catch ( NotModifiedException e )
576         {
577             // Do not cache url here.
578             throw e;
579         }
580         catch ( ProxyException e )
581         {
582             urlFailureCache.cacheFailure( url );
583             throw e;
584         }
585         catch ( WagonFactoryException e )
586         {
587             throw new ProxyException( e.getMessage(), e );
588         }
589         finally
590         {
591             if ( wagon != null )
592             {
593                 try
594                 {
595                     wagon.disconnect();
596                 }
597                 catch ( ConnectionException e )
598                 {
599                     log.warn( "Unable to disconnect wagon.", e );
600                 }
601             }
602         }
603     }
604
605     private void transferArtifact( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath,
606                                    ManagedRepositoryContent repository, Path resource, Path tmpDirectory,
607                                    Path destFile )
608         throws ProxyException
609     {
610         transferSimpleFile( wagon, remoteRepository, remotePath, repository, resource, destFile );
611     }
612
613     private long getLastModified( Path file )
614     {
615         if ( !Files.exists(file) || !Files.isRegularFile(file) )
616         {
617             return 0;
618         }
619
620         try
621         {
622             return Files.getLastModifiedTime(file).toMillis();
623         }
624         catch ( IOException e )
625         {
626             log.error("Could get the modified time of file {}", file.toAbsolutePath());
627             return 0;
628         }
629     }
630
631     private boolean hasBeenUpdated( Path file, long originalLastModified )
632     {
633         if ( !Files.exists(file) || !Files.isRegularFile(file) )
634         {
635             return false;
636         }
637
638         long currentLastModified = getLastModified( file );
639         return ( currentLastModified > originalLastModified );
640     }
641
642     private Path toLocalRepoFile( ManagedRepositoryContent repository, RemoteRepositoryContent targetRepository,
643                                   String targetPath )
644     {
645         String repoPath = metadataTools.getRepositorySpecificName( targetRepository, targetPath );
646         return Paths.get( repository.getRepoRoot(), repoPath );
647     }
648
649     /**
650      * Test if the provided ManagedRepositoryContent has any proxies configured for it.
651      */
652     @Override
653     public boolean hasProxies( ManagedRepositoryContent repository )
654     {
655         synchronized ( this.proxyConnectorMap )
656         {
657             return this.proxyConnectorMap.containsKey( repository.getId() );
658         }
659     }
660
661     private Path toLocalFile( ManagedRepositoryContent repository, ArtifactReference artifact )
662     {
663         return repository.toFile( artifact );
664     }
665
666     /**
667      * Simple method to test if the file exists on the local disk.
668      *
669      * @param file the file to test. (may be null)
670      * @return true if file exists. false if the file param is null, doesn't exist, or is not of type File.
671      */
672     private boolean fileExists( Path file )
673     {
674         if ( file == null )
675         {
676             return false;
677         }
678
679         if ( !Files.exists(file))
680         {
681             return false;
682         }
683
684         return Files.isRegularFile(file);
685     }
686
687     /**
688      * Perform the transfer of the file.
689      *
690      * @param connector         the connector configuration to use.
691      * @param remoteRepository  the remote repository get the resource from.
692      * @param remotePath        the path in the remote repository to the resource to get.
693      * @param repository        the managed repository that will hold the file
694      * @param resource          the local file to place the downloaded resource into
695      * @param requestProperties the request properties to utilize for policy handling.
696      * @param executeConsumers  whether to execute the consumers after proxying
697      * @return the local file that was downloaded, or null if not downloaded.
698      * @throws NotFoundException    if the file was not found on the remote repository.
699      * @throws NotModifiedException if the localFile was present, and the resource was present on remote repository, but
700      *                              the remote resource is not newer than the local File.
701      * @throws ProxyException       if transfer was unsuccessful.
702      */
703     private Path transferFile( ProxyConnector connector, RemoteRepositoryContent remoteRepository, String remotePath,
704                                ManagedRepositoryContent repository, Path resource, Properties requestProperties,
705                                boolean executeConsumers )
706         throws ProxyException, NotModifiedException, RepositoryAdminException
707     {
708         String url = remoteRepository.getURL().getUrl();
709         if ( !url.endsWith( "/" ) )
710         {
711             url = url + "/";
712         }
713         url = url + remotePath;
714         requestProperties.setProperty( "url", url );
715
716         // Is a whitelist defined?
717         if ( CollectionUtils.isNotEmpty( connector.getWhitelist() ) )
718         {
719             // Path must belong to whitelist.
720             if ( !matchesPattern( remotePath, connector.getWhitelist() ) )
721             {
722                 log.debug( "Path [{}] is not part of defined whitelist (skipping transfer from repository [{}]).",
723                            remotePath, remoteRepository.getRepository().getName() );
724                 return null;
725             }
726         }
727
728         // Is target path part of blacklist?
729         if ( matchesPattern( remotePath, connector.getBlacklist() ) )
730         {
731             log.debug( "Path [{}] is part of blacklist (skipping transfer from repository [{}]).", remotePath,
732                        remoteRepository.getRepository().getName() );
733             return null;
734         }
735
736         // Handle pre-download policy
737         try
738         {
739             validatePolicies( this.preDownloadPolicies, connector.getPolicies(), requestProperties, resource );
740         }
741         catch ( PolicyViolationException e )
742         {
743             String emsg = "Transfer not attempted on " + url + " : " + e.getMessage();
744             if ( fileExists( resource ) )
745             {
746                 log.debug( "{} : using already present local file.", emsg );
747                 return resource;
748             }
749
750             log.debug( emsg );
751             return null;
752         }
753
754         Path workingDirectory = createWorkingDirectory( repository );
755         Path tmpResource = workingDirectory.resolve(resource.getFileName());
756         Path tmpMd5 = workingDirectory.resolve(resource.getFileName().toString() + ".md5" );
757         Path tmpSha1 = workingDirectory.resolve( resource.getFileName().toString() + ".sha1" );
758
759         try
760         {
761
762             transferResources( connector, remoteRepository, tmpMd5, tmpSha1, tmpResource, url, remotePath, resource,
763                                workingDirectory, repository );
764
765             // Handle post-download policies.
766             try
767             {
768                 validatePolicies( this.postDownloadPolicies, connector.getPolicies(), requestProperties, tmpResource );
769             }
770             catch ( PolicyViolationException e )
771             {
772                 log.warn( "Transfer invalidated from {} : {}", url, e.getMessage() );
773                 executeConsumers = false;
774                 if ( !fileExists( tmpResource ) )
775                 {
776                     resource = null;
777                 }
778             }
779
780             if ( resource != null )
781             {
782                 synchronized ( resource.toAbsolutePath().toString().intern() )
783                 {
784                     Path directory = resource.getParent();
785                     moveFileIfExists( tmpMd5, directory );
786                     moveFileIfExists( tmpSha1, directory );
787                     moveFileIfExists( tmpResource, directory );
788                 }
789             }
790         }
791         finally
792         {
793             org.apache.archiva.common.utils.FileUtils.deleteQuietly( workingDirectory );
794         }
795
796         if ( executeConsumers )
797         {
798             // Just-in-time update of the index and database by executing the consumers for this artifact
799             //consumers.executeConsumers( connector.getSourceRepository().getRepository(), resource );
800             queueRepositoryTask( connector.getSourceRepository().getRepository().getId(), resource );
801         }
802
803         return resource;
804     }
805
806     private void queueRepositoryTask( String repositoryId, Path localFile )
807     {
808         RepositoryTask task = new RepositoryTask();
809         task.setRepositoryId( repositoryId );
810         task.setResourceFile( localFile.toFile() );
811         task.setUpdateRelatedArtifacts( true );
812         task.setScanAll( true );
813
814         try
815         {
816             scheduler.queueTask( task );
817         }
818         catch ( TaskQueueException e )
819         {
820             log.error( "Unable to queue repository task to execute consumers on resource file ['{}"
821                            + "'].", localFile.getFileName() );
822         }
823     }
824
825     /**
826      * Moves the file into repository location if it exists
827      *
828      * @param fileToMove this could be either the main artifact, sha1 or md5 checksum file.
829      * @param directory  directory to write files to
830      */
831     private void moveFileIfExists( Path fileToMove, Path directory )
832         throws ProxyException
833     {
834         if ( fileToMove != null && Files.exists(fileToMove) )
835         {
836             Path newLocation = directory.resolve(fileToMove.getFileName());
837             moveTempToTarget( fileToMove, newLocation );
838         }
839     }
840
841     /**
842      * <p>
843      * Quietly transfer the checksum file from the remote repository to the local file.
844      * </p>
845      *
846      * @param wagon            the wagon instance (should already be connected) to use.
847      * @param remoteRepository the remote repository to transfer from.
848      * @param remotePath       the remote path to the resource to get.
849      * @param repository       the managed repository that will hold the file
850      * @param resource         the local file that should contain the downloaded contents
851      * @param tmpDirectory     the temporary directory to download to
852      * @param ext              the type of checksum to transfer (example: ".md5" or ".sha1")
853      * @throws ProxyException if copying the downloaded file into place did not succeed.
854      */
855     private void transferChecksum( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath,
856                                    ManagedRepositoryContent repository, Path resource, Path tmpDirectory, String ext,
857                                    Path destFile )
858         throws ProxyException
859     {
860         String url = remoteRepository.getURL().getUrl() + remotePath + ext;
861
862         // Transfer checksum does not use the policy.
863         if ( urlFailureCache.hasFailedBefore( url ) )
864         {
865             return;
866         }
867
868         try
869         {
870             transferSimpleFile( wagon, remoteRepository, remotePath + ext, repository, resource, destFile );
871             log.debug( "Checksum {} Downloaded: {} to move to {}", url, destFile, resource );
872         }
873         catch ( NotFoundException e )
874         {
875             urlFailureCache.cacheFailure( url );
876             log.debug( "Transfer failed, checksum not found: {}", url );
877             // Consume it, do not pass this on.
878         }
879         catch ( NotModifiedException e )
880         {
881             log.debug( "Transfer skipped, checksum not modified: {}", url );
882             // Consume it, do not pass this on.
883         }
884         catch ( ProxyException e )
885         {
886             urlFailureCache.cacheFailure( url );
887             log.warn( "Transfer failed on checksum: {} : {}", url, e.getMessage(), e );
888             // Critical issue, pass it on.
889             throw e;
890         }
891     }
892
893     /**
894      * Perform the transfer of the remote file to the local file specified.
895      *
896      * @param wagon            the wagon instance to use.
897      * @param remoteRepository the remote repository to use
898      * @param remotePath       the remote path to attempt to get
899      * @param repository       the managed repository that will hold the file
900      * @param origFile         the local file to save to
901      * @throws ProxyException if there was a problem moving the downloaded file into place.
902      */
903     private void transferSimpleFile( Wagon wagon, RemoteRepositoryContent remoteRepository, String remotePath,
904                                      ManagedRepositoryContent repository, Path origFile, Path destFile )
905         throws ProxyException
906     {
907         assert ( remotePath != null );
908
909         // Transfer the file.
910         try
911         {
912             boolean success = false;
913
914             if ( !Files.exists(origFile))
915             {
916                 log.debug( "Retrieving {} from {}", remotePath, remoteRepository.getRepository().getName() );
917                 wagon.get( addParameters( remotePath, remoteRepository.getRepository() ), destFile.toFile() );
918                 success = true;
919
920                 // You wouldn't get here on failure, a WagonException would have been thrown.
921                 log.debug( "Downloaded successfully." );
922             }
923             else
924             {
925                 log.debug( "Retrieving {} from {} if updated", remotePath, remoteRepository.getRepository().getName() );
926                 try
927                 {
928                     success = wagon.getIfNewer( addParameters( remotePath, remoteRepository.getRepository() ), destFile.toFile(),
929                                                 Files.getLastModifiedTime(origFile).toMillis());
930                 }
931                 catch ( IOException e )
932                 {
933                     throw new ProxyException( "Failed to the modification time of "+origFile.toAbsolutePath() );
934                 }
935                 if ( !success )
936                 {
937                     throw new NotModifiedException(
938                         "Not downloaded, as local file is newer than remote side: " + origFile.toAbsolutePath() );
939                 }
940
941                 if ( Files.exists(destFile))
942                 {
943                     log.debug( "Downloaded successfully." );
944                 }
945             }
946         }
947         catch ( ResourceDoesNotExistException e )
948         {
949             throw new NotFoundException(
950                 "Resource [" + remoteRepository.getURL() + "/" + remotePath + "] does not exist: " + e.getMessage(),
951                 e );
952         }
953         catch ( WagonException e )
954         {
955             // TODO: shouldn't have to drill into the cause, but TransferFailedException is often not descriptive enough
956
957             String msg =
958                 "Download failure on resource [" + remoteRepository.getURL() + "/" + remotePath + "]:" + e.getMessage();
959             if ( e.getCause() != null )
960             {
961                 msg += " (cause: " + e.getCause() + ")";
962             }
963             throw new ProxyException( msg, e );
964         }
965     }
966
967     /**
968      * Apply the policies.
969      *
970      * @param policies  the map of policies to execute. (Map of String policy keys, to {@link DownloadPolicy} objects)
971      * @param settings  the map of settings for the policies to execute. (Map of String policy keys, to String policy
972      *                  setting)
973      * @param request   the request properties (utilized by the {@link DownloadPolicy#applyPolicy(String, Properties, Path)}
974      *                  )
975      * @param localFile the local file (utilized by the {@link DownloadPolicy#applyPolicy(String, Properties, Path)})
976      * @throws PolicyViolationException
977      */
978     private void validatePolicies( Map<String, ? extends DownloadPolicy> policies, Map<String, String> settings,
979                                    Properties request, Path localFile )
980         throws PolicyViolationException
981     {
982         for ( Entry<String, ? extends DownloadPolicy> entry : policies.entrySet() )
983         {
984             // olamy with spring rolehint is now downloadPolicy#hint
985             // so substring after last # to get the hint as with plexus
986             String key = StringUtils.substringAfterLast( entry.getKey(), "#" );
987             DownloadPolicy policy = entry.getValue();
988             String defaultSetting = policy.getDefaultOption();
989
990             String setting = StringUtils.defaultString( settings.get( key ), defaultSetting );
991
992             log.debug( "Applying [{}] policy with [{}]", key, setting );
993             try
994             {
995                 policy.applyPolicy( setting, request, localFile );
996             }
997             catch ( PolicyConfigurationException e )
998             {
999                 log.error( e.getMessage(), e );
1000             }
1001         }
1002     }
1003
1004     private void validatePolicies( Map<String, DownloadErrorPolicy> policies, Map<String, String> settings,
1005                                    Properties request, ArtifactReference artifact, RemoteRepositoryContent content,
1006                                    Path localFile, Exception exception, Map<String, Exception> previousExceptions )
1007         throws ProxyDownloadException
1008     {
1009         boolean process = true;
1010         for ( Entry<String, ? extends DownloadErrorPolicy> entry : policies.entrySet() )
1011         {
1012
1013             // olamy with spring rolehint is now downloadPolicy#hint
1014             // so substring after last # to get the hint as with plexus
1015             String key = StringUtils.substringAfterLast( entry.getKey(), "#" );
1016             DownloadErrorPolicy policy = entry.getValue();
1017             String defaultSetting = policy.getDefaultOption();
1018             String setting = StringUtils.defaultString( settings.get( key ), defaultSetting );
1019
1020             log.debug( "Applying [{}] policy with [{}]", key, setting );
1021             try
1022             {
1023                 // all policies must approve the exception, any can cancel
1024                 process = policy.applyPolicy( setting, request, localFile, exception, previousExceptions );
1025                 if ( !process )
1026                 {
1027                     break;
1028                 }
1029             }
1030             catch ( PolicyConfigurationException e )
1031             {
1032                 log.error( e.getMessage(), e );
1033             }
1034         }
1035
1036         if ( process )
1037         {
1038             // if the exception was queued, don't throw it
1039             if ( !previousExceptions.containsKey( content.getId() ) )
1040             {
1041                 throw new ProxyDownloadException(
1042                     "An error occurred in downloading from the remote repository, and the policy is to fail immediately",
1043                     content.getId(), exception );
1044             }
1045         }
1046         else
1047         {
1048             // if the exception was queued, but cancelled, remove it
1049             previousExceptions.remove( content.getId() );
1050         }
1051
1052         log.warn(
1053             "Transfer error from repository {} for artifact {} , continuing to next repository. Error message: {}",
1054             content.getRepository().getId(), Keys.toKey( artifact ), exception.getMessage() );
1055         log.debug( "Full stack trace", exception );
1056     }
1057
1058     /**
1059      * Creates a working directory
1060      *
1061      * @param repository
1062      * @return file location of working directory
1063      */
1064     private Path createWorkingDirectory( ManagedRepositoryContent repository )
1065     {
1066         try
1067         {
1068             return Files.createTempDirectory( "temp" );
1069         }
1070         catch ( IOException e )
1071         {
1072             throw new RuntimeException( e.getMessage(), e );
1073         }
1074
1075     }
1076
1077     /**
1078      * Used to move the temporary file to its real destination. This is patterned from the way WagonManager handles its
1079      * downloaded files.
1080      *
1081      * @param temp   The completed download file
1082      * @param target The final location of the downloaded file
1083      * @throws ProxyException when the temp file cannot replace the target file
1084      */
1085     private void moveTempToTarget( Path temp, Path target )
1086         throws ProxyException
1087     {
1088
1089         Lock lock;
1090         try
1091         {
1092             lock = fileLockManager.writeFileLock( target.toFile() );
1093             if ( lock.getFile().exists() && !lock.getFile().delete() )
1094             {
1095                 throw new ProxyException( "Unable to overwrite existing target file: " + target.toAbsolutePath() );
1096             }
1097
1098             lock.getFile().getParentFile().mkdirs();
1099
1100             try
1101             {
1102                 Files.move(temp, lock.getFile().toPath() );
1103             }
1104             catch ( IOException e )
1105             {
1106                 log.warn( "Unable to rename tmp file to its final name... resorting to copy command." );
1107
1108                 try
1109                 {
1110                     Files.copy( temp, lock.getFile().toPath() );
1111                 }
1112                 catch ( IOException e2 )
1113                 {
1114                     if ( lock.getFile().exists() )
1115                     {
1116                         log.debug( "Tried to copy file {} to {} but file with this name already exists.",
1117                             temp.getFileName(), lock.getFile().getAbsolutePath() );
1118                     }
1119                     else
1120                     {
1121                         throw new ProxyException(
1122                             "Cannot copy tmp file " + temp.toAbsolutePath() + " to its final location", e2 );
1123                     }
1124                 }
1125                 finally
1126                 {
1127                     org.apache.archiva.common.utils.FileUtils.deleteQuietly( temp );
1128                 }
1129             }
1130
1131         }
1132         catch ( FileLockException | FileLockTimeoutException e )
1133         {
1134             throw new ProxyException( e.getMessage(), e );
1135         }
1136     }
1137
1138     /**
1139      * Using wagon, connect to the remote repository.
1140      *
1141      * @param connector        the connector configuration to utilize (for obtaining network proxy configuration from)
1142      * @param wagon            the wagon instance to establish the connection on.
1143      * @param remoteRepository the remote repository to connect to.
1144      * @return true if the connection was successful. false if not connected.
1145      */
1146     private boolean connectToRepository( ProxyConnector connector, Wagon wagon,
1147                                          RemoteRepositoryContent remoteRepository )
1148     {
1149         boolean connected = false;
1150
1151         final ProxyInfo networkProxy =
1152             connector.getProxyId() == null ? null : this.networkProxyMap.get( connector.getProxyId() );
1153
1154         if ( log.isDebugEnabled() )
1155         {
1156             if ( networkProxy != null )
1157             {
1158                 // TODO: move to proxyInfo.toString()
1159                 String msg = "Using network proxy " + networkProxy.getHost() + ":" + networkProxy.getPort()
1160                     + " to connect to remote repository " + remoteRepository.getURL();
1161                 if ( networkProxy.getNonProxyHosts() != null )
1162                 {
1163                     msg += "; excluding hosts: " + networkProxy.getNonProxyHosts();
1164                 }
1165                 if ( StringUtils.isNotBlank( networkProxy.getUserName() ) )
1166                 {
1167                     msg += "; as user: " + networkProxy.getUserName();
1168                 }
1169                 log.debug( msg );
1170             }
1171         }
1172
1173         AuthenticationInfo authInfo = null;
1174         String username = remoteRepository.getRepository().getUserName();
1175         String password = remoteRepository.getRepository().getPassword();
1176
1177         if ( StringUtils.isNotBlank( username ) && StringUtils.isNotBlank( password ) )
1178         {
1179             log.debug( "Using username {} to connect to remote repository {}", username, remoteRepository.getURL() );
1180             authInfo = new AuthenticationInfo();
1181             authInfo.setUserName( username );
1182             authInfo.setPassword( password );
1183         }
1184
1185         // Convert seconds to milliseconds
1186         long timeoutInMilliseconds = TimeUnit.MILLISECONDS.convert( remoteRepository.getRepository().getTimeout(), //
1187                                                                     TimeUnit.SECONDS );
1188
1189         // Set timeout  read and connect
1190         // FIXME olamy having 2 config values
1191         wagon.setReadTimeout( (int) timeoutInMilliseconds );
1192         wagon.setTimeout( (int)  timeoutInMilliseconds );
1193
1194         try
1195         {
1196             Repository wagonRepository =
1197                 new Repository( remoteRepository.getId(), remoteRepository.getURL().toString() );
1198             wagon.connect( wagonRepository, authInfo, networkProxy );
1199             connected = true;
1200         }
1201         catch ( ConnectionException | AuthenticationException e )
1202         {
1203             log.warn( "Could not connect to {}: {}", remoteRepository.getRepository().getName(), e.getMessage() );
1204             connected = false;
1205         }
1206
1207         return connected;
1208     }
1209
1210     /**
1211      * Tests whitelist and blacklist patterns against path.
1212      *
1213      * @param path     the path to test.
1214      * @param patterns the list of patterns to check.
1215      * @return true if the path matches at least 1 pattern in the provided patterns list.
1216      */
1217     private boolean matchesPattern( String path, List<String> patterns )
1218     {
1219         if ( CollectionUtils.isEmpty( patterns ) )
1220         {
1221             return false;
1222         }
1223
1224         if ( !path.startsWith( "/" ) )
1225         {
1226             path = "/" + path;
1227         }
1228
1229         for ( String pattern : patterns )
1230         {
1231             if ( !pattern.startsWith( "/" ) )
1232             {
1233                 pattern = "/" + pattern;
1234             }
1235
1236             if ( SelectorUtils.matchPath( pattern, path, false ) )
1237             {
1238                 return true;
1239             }
1240         }
1241
1242         return false;
1243     }
1244
1245     /**
1246      * TODO: Ensure that list is correctly ordered based on configuration. See MRM-477
1247      */
1248     @Override
1249     public List<ProxyConnector> getProxyConnectors( ManagedRepositoryContent repository )
1250     {
1251
1252         if ( !this.proxyConnectorMap.containsKey( repository.getId() ) )
1253         {
1254             return Collections.emptyList();
1255         }
1256         List<ProxyConnector> ret = new ArrayList<>( this.proxyConnectorMap.get( repository.getId() ) );
1257
1258         Collections.sort( ret, ProxyConnectorOrderComparator.getInstance() );
1259         return ret;
1260
1261     }
1262
1263     @Override
1264     public void afterConfigurationChange( Registry registry, String propertyName, Object propertyValue )
1265     {
1266         if ( ConfigurationNames.isNetworkProxy( propertyName ) //
1267             || ConfigurationNames.isManagedRepositories( propertyName ) //
1268             || ConfigurationNames.isRemoteRepositories( propertyName ) //
1269             || ConfigurationNames.isProxyConnector( propertyName ) ) //
1270         {
1271             initConnectorsAndNetworkProxies();
1272         }
1273     }
1274
1275     protected String addParameters( String path, RemoteRepository remoteRepository )
1276     {
1277         if ( remoteRepository.getExtraParameters().isEmpty() )
1278         {
1279             return path;
1280         }
1281
1282         boolean question = false;
1283
1284         StringBuilder res = new StringBuilder( path == null ? "" : path );
1285
1286         for ( Entry<String, String> entry : remoteRepository.getExtraParameters().entrySet() )
1287         {
1288             if ( !question )
1289             {
1290                 res.append( '?' ).append( entry.getKey() ).append( '=' ).append( entry.getValue() );
1291             }
1292         }
1293
1294         return res.toString();
1295     }
1296
1297
1298     @Override
1299     public void beforeConfigurationChange( Registry registry, String propertyName, Object propertyValue )
1300     {
1301         /* do nothing */
1302     }
1303
1304     public ArchivaConfiguration getArchivaConfiguration()
1305     {
1306         return archivaConfiguration;
1307     }
1308
1309     public void setArchivaConfiguration( ArchivaConfiguration archivaConfiguration )
1310     {
1311         this.archivaConfiguration = archivaConfiguration;
1312     }
1313
1314     public RepositoryContentFactory getRepositoryFactory()
1315     {
1316         return repositoryFactory;
1317     }
1318
1319     public void setRepositoryFactory( RepositoryContentFactory repositoryFactory )
1320     {
1321         this.repositoryFactory = repositoryFactory;
1322     }
1323
1324     public MetadataTools getMetadataTools()
1325     {
1326         return metadataTools;
1327     }
1328
1329     public void setMetadataTools( MetadataTools metadataTools )
1330     {
1331         this.metadataTools = metadataTools;
1332     }
1333
1334     public UrlFailureCache getUrlFailureCache()
1335     {
1336         return urlFailureCache;
1337     }
1338
1339     public void setUrlFailureCache( UrlFailureCache urlFailureCache )
1340     {
1341         this.urlFailureCache = urlFailureCache;
1342     }
1343
1344     public WagonFactory getWagonFactory()
1345     {
1346         return wagonFactory;
1347     }
1348
1349     public void setWagonFactory( WagonFactory wagonFactory )
1350     {
1351         this.wagonFactory = wagonFactory;
1352     }
1353
1354     public Map<String, PreDownloadPolicy> getPreDownloadPolicies()
1355     {
1356         return preDownloadPolicies;
1357     }
1358
1359     public void setPreDownloadPolicies( Map<String, PreDownloadPolicy> preDownloadPolicies )
1360     {
1361         this.preDownloadPolicies = preDownloadPolicies;
1362     }
1363
1364     public Map<String, PostDownloadPolicy> getPostDownloadPolicies()
1365     {
1366         return postDownloadPolicies;
1367     }
1368
1369     public void setPostDownloadPolicies( Map<String, PostDownloadPolicy> postDownloadPolicies )
1370     {
1371         this.postDownloadPolicies = postDownloadPolicies;
1372     }
1373
1374     public Map<String, DownloadErrorPolicy> getDownloadErrorPolicies()
1375     {
1376         return downloadErrorPolicies;
1377     }
1378
1379     public void setDownloadErrorPolicies( Map<String, DownloadErrorPolicy> downloadErrorPolicies )
1380     {
1381         this.downloadErrorPolicies = downloadErrorPolicies;
1382     }
1383 }