package org.apache.archiva.webdav; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.archiva.admin.model.managed.ManagedRepositoryAdmin; import org.apache.archiva.admin.model.remote.RemoteRepositoryAdmin; import org.apache.archiva.audit.Auditable; import org.apache.archiva.checksum.ChecksumAlgorithm; import org.apache.archiva.checksum.ChecksumUtil; import org.apache.archiva.checksum.StreamingChecksum; import org.apache.archiva.common.filelock.DefaultFileLockManager; import org.apache.archiva.common.filelock.FileLockManager; import org.apache.archiva.common.utils.PathUtil; import org.apache.archiva.common.utils.VersionUtil; import org.apache.archiva.configuration.ArchivaConfiguration; import org.apache.archiva.indexer.ArchivaIndexingContext; import org.apache.archiva.indexer.merger.IndexMerger; import org.apache.archiva.indexer.merger.IndexMergerException; import org.apache.archiva.indexer.merger.IndexMergerRequest; import org.apache.archiva.indexer.merger.TemporaryGroupIndex; import org.apache.archiva.indexer.merger.base.MergedRemoteIndexesTask; import org.apache.archiva.indexer.merger.base.MergedRemoteIndexesTaskRequest; import org.apache.archiva.indexer.search.RepositorySearch; import org.apache.archiva.indexer.search.RepositorySearchException; import org.apache.archiva.metadata.audit.AuditListener; import org.apache.archiva.metadata.model.facets.AuditEvent; import org.apache.archiva.metadata.repository.storage.RelocationException; import org.apache.archiva.metadata.repository.storage.RepositoryStorage; import org.apache.archiva.model.ArchivaRepositoryMetadata; import org.apache.archiva.policies.ProxyDownloadException; import org.apache.archiva.proxy.ProxyRegistry; import org.apache.archiva.proxy.model.RepositoryProxyHandler; import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.authentication.AuthenticationResult; import org.apache.archiva.redback.authorization.AuthorizationException; import org.apache.archiva.redback.authorization.UnauthorizedException; import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator; import org.apache.archiva.redback.policy.AccountLockedException; import org.apache.archiva.redback.policy.MustChangePasswordException; import org.apache.archiva.redback.system.SecuritySession; import org.apache.archiva.redback.users.User; import org.apache.archiva.redback.users.UserManager; import org.apache.archiva.repository.content.BaseRepositoryContentLayout; import org.apache.archiva.repository.content.ContentAccessException; import org.apache.archiva.repository.content.LayoutException; import org.apache.archiva.repository.ManagedRepository; import org.apache.archiva.repository.ManagedRepositoryContent; import org.apache.archiva.repository.ReleaseScheme; import org.apache.archiva.repository.RepositoryGroup; import org.apache.archiva.repository.RepositoryRegistry; import org.apache.archiva.repository.RepositoryRequestInfo; import org.apache.archiva.repository.content.Artifact; import org.apache.archiva.repository.content.ContentItem; import org.apache.archiva.repository.content.ItemSelector; import org.apache.archiva.repository.features.IndexCreationFeature; import org.apache.archiva.repository.metadata.RepositoryMetadataException; import org.apache.archiva.repository.metadata.base.MetadataTools; import org.apache.archiva.repository.metadata.base.RepositoryMetadataMerge; import org.apache.archiva.repository.metadata.base.RepositoryMetadataWriter; import org.apache.archiva.repository.storage.StorageAsset; import org.apache.archiva.repository.storage.fs.FilesystemStorage; import org.apache.archiva.scheduler.repository.model.RepositoryArchivaTaskScheduler; import org.apache.archiva.security.ServletAuthenticator; import org.apache.archiva.webdav.util.MimeTypes; import org.apache.archiva.webdav.util.TemporaryGroupIndexSessionCleaner; import org.apache.archiva.webdav.util.WebdavMethodUtil; import org.apache.archiva.xml.XMLException; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceFactory; import org.apache.jackrabbit.webdav.DavResourceLocator; import org.apache.jackrabbit.webdav.DavServletRequest; import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.lock.LockManager; import org.apache.jackrabbit.webdav.lock.SimpleLockManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MarkerFactory; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * */ @Service( "davResourceFactory#archiva" ) public class ArchivaDavResourceFactory implements DavResourceFactory, Auditable { private static final String PROXIED_SUFFIX = " (proxied)"; private static final String HTTP_PUT_METHOD = "PUT"; private Logger log = LoggerFactory.getLogger( ArchivaDavResourceFactory.class ); @Inject private List<AuditListener> auditListeners = new ArrayList<>(); @Inject private ProxyRegistry proxyRegistry; @Inject private MetadataTools metadataTools; @Inject private MimeTypes mimeTypes; private ArchivaConfiguration archivaConfiguration; @Inject private ServletAuthenticator servletAuth; @Inject @Named( value = "httpAuthenticator#basic" ) private HttpAuthenticator httpAuth; @Inject private RemoteRepositoryAdmin remoteRepositoryAdmin; @Inject private ManagedRepositoryAdmin managedRepositoryAdmin; @Inject private RepositoryRegistry repositoryRegistry; @Inject private IndexMerger indexMerger; @Inject private RepositorySearch repositorySearch; /** * Lock Manager - use simple implementation from JackRabbit */ private final LockManager lockManager = new SimpleLockManager(); @Inject @Named( value = "archivaTaskScheduler#repository" ) private RepositoryArchivaTaskScheduler scheduler; @Inject @Named( value = "fileLockManager#default" ) private FileLockManager fileLockManager; private ApplicationContext applicationContext; @Inject public ArchivaDavResourceFactory( ApplicationContext applicationContext, ArchivaConfiguration archivaConfiguration ) { this.archivaConfiguration = archivaConfiguration; this.applicationContext = applicationContext; } @PostConstruct public void initialize() throws IOException { } @Override public DavResource createResource( final DavResourceLocator locator, final DavServletRequest request, final DavServletResponse response ) throws DavException { final ArchivaDavResourceLocator archivaLocator = checkLocatorIsInstanceOfRepositoryLocator( locator ); final String sRepoId = archivaLocator.getRepositoryId(); RepositoryGroup repoGroup = repositoryRegistry.getRepositoryGroup(sRepoId); final boolean isGroupRepo = repoGroup != null; String activePrincipal = getActivePrincipal( request ); List<String> resourcesInAbsolutePath = new ArrayList<>(); boolean readMethod = WebdavMethodUtil.isReadMethod( request.getMethod() ); RepositoryRequestInfo repositoryRequestInfo = null; DavResource resource; if ( isGroupRepo ) { if ( !readMethod ) { throw new DavException( HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Write method not allowed for repository groups." ); } log.debug( "Repository group '{}' accessed by '{}", repoGroup.getId(), activePrincipal ); // handle browse requests for virtual repos if ( getLogicalResource( archivaLocator, null, true ).endsWith( "/" ) ) { DavResource davResource = getResourceFromGroup( request, archivaLocator, repoGroup ); setHeaders( response, locator, davResource, true ); return davResource; } else { // make a copy to avoid potential concurrent modifications (eg. by configuration) // TODO: ultimately, locking might be more efficient than copying in this fashion since updates are // infrequent resource = processRepositoryGroup( request, archivaLocator, activePrincipal, resourcesInAbsolutePath, repoGroup ); for (ManagedRepository repo : repoGroup.getRepositories() ) { if (repo!=null) { repositoryRequestInfo = repo.getRequestInfo(); break; } } } } else { // We do not provide folders for remote repositories ManagedRepository repo = repositoryRegistry.getManagedRepository( sRepoId ); if (repo==null) { throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Invalid repository: " + archivaLocator.getRepositoryId() ); } ManagedRepositoryContent managedRepositoryContent = repo.getContent( ); if (managedRepositoryContent==null) { log.error("Inconsistency detected. Repository content not found for '{}'", archivaLocator.getRepositoryId()); throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Invalid repository: " + archivaLocator.getRepositoryId() ); } log.debug( "Managed repository '{}' accessed by '{}'", managedRepositoryContent.getId(), activePrincipal ); resource = processRepository( request, archivaLocator, activePrincipal, managedRepositoryContent, repo); repositoryRequestInfo = repo.getRequestInfo(); String logicalResource = getLogicalResource( archivaLocator, null, false ); resourcesInAbsolutePath.add( managedRepositoryContent.getRepository().getRoot().getFilePath().resolve(logicalResource ).toAbsolutePath().toString() ); } String requestedResource = request.getRequestURI(); // MRM-872 : merge all available metadata // merge metadata only when requested via the repo group if ( ( repositoryRequestInfo.isMetadata( requestedResource ) || repositoryRequestInfo.isMetadataSupportFile( requestedResource ) ) && isGroupRepo ) { // this should only be at the project level not version level! if ( isProjectReference( requestedResource ) ) { ArchivaDavResource res = (ArchivaDavResource) resource; String newPath; if (res.getAsset().hasParent()) { newPath = res.getAsset( ).getParent( ).getPath( ) + "/maven-metadata-" + sRepoId + ".xml"; } else { newPath = StringUtils.substringBeforeLast( res.getAsset().getPath(), "/" ) + "/maven-metadata-" + sRepoId + ".xml";; } // for MRM-872 handle checksums of the merged metadata files if ( repositoryRequestInfo.isSupportFile( requestedResource ) ) { String metadataChecksumPath = newPath + "." + StringUtils.substringAfterLast( requestedResource, "." ); StorageAsset metadataChecksum = repoGroup.getAsset( metadataChecksumPath ); if ( repoGroup.getAsset( metadataChecksumPath ).exists() ) { LogicalResource logicalResource = new LogicalResource( getLogicalResource( archivaLocator, null, false ) ); try { resource = new ArchivaDavResource( metadataChecksum, logicalResource.getPath(), repoGroup, request.getRemoteAddr(), activePrincipal, request.getDavSession(), archivaLocator, this, mimeTypes, auditListeners, scheduler); } catch ( LayoutException e ) { log.error("Incompatible layout: {}", e.getMessage(), e); throw new DavException( 500, e ); } } } else { if ( resourcesInAbsolutePath != null && resourcesInAbsolutePath.size() > 1 ) { // merge the metadata of all repos under group ArchivaRepositoryMetadata mergedMetadata = new ArchivaRepositoryMetadata(); for ( String resourceAbsPath : resourcesInAbsolutePath ) { try { Path metadataFile = Paths.get( resourceAbsPath ); FilesystemStorage storage = new FilesystemStorage( metadataFile.getParent( ), new DefaultFileLockManager( ) ); ArchivaRepositoryMetadata repoMetadata = repositoryRegistry.getMetadataReader( repoGroup.getType( ) ).read( storage.getAsset( metadataFile.getFileName().toString() ) ); mergedMetadata = RepositoryMetadataMerge.merge( mergedMetadata, repoMetadata ); } catch ( RepositoryMetadataException r ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error occurred while merging metadata file." ); } catch ( IOException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error occurred while merging metadata file." ); } } try { StorageAsset resourceFile = writeMergedMetadataToFile( repoGroup, mergedMetadata, newPath ); LogicalResource logicalResource = new LogicalResource( getLogicalResource( archivaLocator, null, false ) ); resource = new ArchivaDavResource( resourceFile, logicalResource.getPath(), repoGroup, request.getRemoteAddr(), activePrincipal, request.getDavSession(), archivaLocator, this, mimeTypes, auditListeners, scheduler); } catch ( RepositoryMetadataException r ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error occurred while writing metadata file." ); } catch ( IOException ie ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error occurred while generating checksum files." ); } catch ( LayoutException e ) { log.error("Incompatible layout: {}", e.getMessage(), e); throw new DavException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Incompatible layout for repository "+repoGroup.getId()); } } } } } setHeaders( response, locator, resource, false ); // compatibility with MRM-440 to ensure browsing the repository works ok if ( resource.isCollection() && !request.getRequestURI().endsWith( "/" ) ) { throw new BrowserRedirectException( resource.getHref() ); } resource.addLockManager( lockManager ); return resource; } private DavResource processRepositoryGroup( final DavServletRequest request, ArchivaDavResourceLocator archivaLocator, String activePrincipal, List<String> resourcesInAbsolutePath, RepositoryGroup repoGroup ) throws DavException { DavResource resource = null; List<DavException> storedExceptions = new ArrayList<>(); String pathInfo = StringUtils.removeEnd( request.getPathInfo(), "/" ); String rootPath = StringUtils.substringBeforeLast( pathInfo, "/" ); String mergedIndexPath = "/"; if (repoGroup.supportsFeature( IndexCreationFeature.class )) { mergedIndexPath = repoGroup.getFeature( IndexCreationFeature.class ).get().getIndexPath().getPath(); } if ( StringUtils.endsWith( rootPath, mergedIndexPath ) ) { // we are in the case of index file request String requestedFileName = StringUtils.substringAfterLast( pathInfo, "/" ); StorageAsset temporaryIndexDirectory = buildMergedIndexDirectory( activePrincipal, request, repoGroup ); StorageAsset asset = temporaryIndexDirectory.resolve(requestedFileName); try { resource = new ArchivaDavResource( asset, requestedFileName, repoGroup, request.getRemoteAddr(), activePrincipal, request.getDavSession(), archivaLocator, this, mimeTypes, auditListeners, scheduler ); } catch (LayoutException e) { log.error("Bad layout: {}", e.getMessage(), e); throw new DavException(500, e); } } else { for ( ManagedRepository repository : repoGroup.getRepositories() ) { String repositoryId = repository.getId(); ManagedRepositoryContent managedRepositoryContent; ManagedRepository managedRepository = repositoryRegistry.getManagedRepository( repositoryId ); if (managedRepository==null) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not find repository with id "+repositoryId ); } managedRepositoryContent = managedRepository.getContent(); if (managedRepositoryContent==null) { log.error("Inconsistency detected. Repository content not found for '{}'",repositoryId); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not find repository content with id "+repositoryId ); } try { DavResource updatedResource = processRepository( request, archivaLocator, activePrincipal, managedRepositoryContent, managedRepository ); if ( resource == null ) { resource = updatedResource; } String logicalResource = getLogicalResource( archivaLocator, null, false ); if ( logicalResource.endsWith( "/" ) ) { logicalResource = logicalResource.substring( 1 ); } resourcesInAbsolutePath.add( managedRepositoryContent.getRepository().getRoot().resolve( logicalResource ).getFilePath().toAbsolutePath().toString() ); } catch ( DavException e ) { storedExceptions.add( e ); } } } if ( resource == null ) { if ( !storedExceptions.isEmpty() ) { // MRM-1232 for ( DavException e : storedExceptions ) { if ( 401 == e.getErrorCode() ) { throw e; } } throw new DavException( HttpServletResponse.SC_NOT_FOUND ); } else { throw new DavException( HttpServletResponse.SC_NOT_FOUND ); } } return resource; } private String getLogicalResource( ArchivaDavResourceLocator archivaLocator, org.apache.archiva.repository.ManagedRepository managedRepository, boolean useOrigResourcePath ) { // FIXME remove this hack // but currently managedRepository can be null in case of group String layout = managedRepository == null ? "default" : managedRepository.getLayout(); RepositoryStorage repositoryStorage = this.applicationContext.getBean( "repositoryStorage#" + layout, RepositoryStorage.class ); String path = repositoryStorage.getFilePath( useOrigResourcePath ? archivaLocator.getOrigResourcePath() : archivaLocator.getResourcePath(), managedRepository ); log.debug( "found path {} for resourcePath: '{}' with managedRepo '{}' and layout '{}'", path, archivaLocator.getResourcePath(), managedRepository == null ? "null" : managedRepository.getId(), layout ); return path; } private String evaluatePathWithVersion( ArchivaDavResourceLocator archivaLocator, // ManagedRepositoryContent managedRepositoryContent, // String contextPath ) throws DavException { String layout = managedRepositoryContent.getRepository() == null ? "default" : managedRepositoryContent.getRepository().getLayout(); RepositoryStorage repositoryStorage = this.applicationContext.getBean( "repositoryStorage#" + layout, RepositoryStorage.class ); try { return repositoryStorage.getFilePathWithVersion( archivaLocator.getResourcePath(), // managedRepositoryContent ); } catch ( RelocationException e ) { String path = e.getPath(); log.debug( "Relocation to {}", path ); throw new BrowserRedirectException( addHrefPrefix( contextPath, path ), e.getRelocationType() ); } catch (XMLException | IOException e ) { log.error( e.getMessage(), e ); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } } private DavResource processRepository( final DavServletRequest request, ArchivaDavResourceLocator archivaLocator, String activePrincipal, ManagedRepositoryContent managedRepositoryContent, org.apache.archiva.repository.ManagedRepository managedRepository ) throws DavException { DavResource resource = null; if ( isAuthorized( request, managedRepositoryContent.getId() ) ) { boolean readMethod = WebdavMethodUtil.isReadMethod( request.getMethod() ); // Maven Centric part ask evaluation if -SNAPSHOT // MRM-1846 test if read method to prevent issue with maven 2.2.1 and uniqueVersion false String path = readMethod ? evaluatePathWithVersion( archivaLocator, managedRepositoryContent, request.getContextPath() ) : getLogicalResource( archivaLocator, managedRepository, false ); if ( path.startsWith( "/" ) ) { path = path.substring( 1 ); } LogicalResource logicalResource = new LogicalResource( path ); StorageAsset repoAsset = managedRepository.getAsset( path ); // Path resourceFile = Paths.get( managedRepositoryContent.getRepoRoot(), path ); try { resource = new ArchivaDavResource( repoAsset, path, managedRepository, request.getRemoteAddr(), activePrincipal, request.getDavSession(), archivaLocator, this, mimeTypes, auditListeners, scheduler ); } catch ( LayoutException e ) { log.error("Incompatible layout: {}", e.getMessage(), e); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } if ( WebdavMethodUtil.isReadMethod( request.getMethod() ) ) { if ( archivaLocator.getHref( false ).endsWith( "/" ) && !repoAsset.isContainer() ) { // force a resource not found throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource does not exist" ); } else { if ( !resource.isCollection() ) { boolean previouslyExisted = repoAsset.exists(); boolean fromProxy = fetchContentFromProxies( managedRepository, request, logicalResource ); StorageAsset resourceAsset=null; // At this point the incoming request can either be in default or // legacy layout format. try { // Perform an adjustment of the resource to the managed // repository expected path. // String localResourcePath = managedRepository.getRequestInfo().toNativePath( logicalResource.getPath() ); resourceAsset = managedRepository.getAsset( logicalResource.getPath() ); resource = new ArchivaDavResource( resourceAsset, logicalResource.getPath(), managedRepository, request.getRemoteAddr(), activePrincipal, request.getDavSession(), archivaLocator, this, mimeTypes, auditListeners, scheduler ); } catch ( LayoutException e ) { if ( resourceAsset==null || !resourceAsset.exists() ) { throw new DavException( HttpServletResponse.SC_NOT_FOUND, e ); } } if ( fromProxy ) { String action = ( previouslyExisted ? AuditEvent.MODIFY_FILE : AuditEvent.CREATE_FILE ) + PROXIED_SUFFIX; log.debug( "Proxied artifact '{}' in repository '{}' (current user '{}')", resourceAsset.getName(), managedRepositoryContent.getId(), activePrincipal ); triggerAuditEvent( request.getRemoteAddr(), archivaLocator.getRepositoryId(), logicalResource.getPath(), action, activePrincipal ); } if ( !resourceAsset.exists() ) { throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource does not exist" ); } } } } if ( request.getMethod().equals( HTTP_PUT_METHOD ) ) { String resourcePath = logicalResource.getPath(); RepositoryRequestInfo repositoryRequestInfo = managedRepository.getRequestInfo(); // check if target repo is enabled for releases // we suppose that release-artifacts can be deployed only to repos enabled for releases if ( managedRepositoryContent.getRepository().getActiveReleaseSchemes().contains( ReleaseScheme.RELEASE ) && !repositoryRequestInfo.isMetadata( resourcePath ) && !repositoryRequestInfo.isSupportFile( resourcePath ) ) { // ArtifactReference artifact = null; Artifact artifact = null; try { BaseRepositoryContentLayout layout = managedRepositoryContent.getLayout( BaseRepositoryContentLayout.class ); ContentItem artifactItem = managedRepositoryContent.toItem( resourcePath ); artifact = layout.adaptItem( Artifact.class, artifactItem ); if ( !VersionUtil.isSnapshot( artifact.getVersion().getId() ) ) { // check if artifact already exists and if artifact re-deployment to the repository is allowed if ( artifactItem.exists() && managedRepositoryContent.getRepository().blocksRedeployments()) { log.warn( "Overwriting released artifacts in repository '{}' is not allowed.", managedRepositoryContent.getId() ); throw new DavException( HttpServletResponse.SC_CONFLICT, "Overwriting released artifacts is not allowed." ); } } } catch ( LayoutException e ) { log.warn( "Artifact path '{}' is invalid.", resourcePath ); } catch ( ContentAccessException e ) { e.printStackTrace( ); } } /* * Create parent directories that don't exist when writing a file This actually makes this * implementation not compliant to the WebDAV RFC - but we have enough knowledge about how the * collection is being used to do this reasonably and some versions of Maven's WebDAV don't correctly * create the collections themselves. */ StorageAsset rootDirectory = managedRepositoryContent.getRepository( ).getRoot(); StorageAsset destDir = rootDirectory.resolve( logicalResource.getPath() ).getParent(); if ( !destDir.exists() ) { try { destDir.create(); } catch ( IOException e ) { log.error("Could not create directory {}: {}", destDir, e.getMessage(), e); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not create directory "+destDir ); } String relPath = PathUtil.getRelative( rootDirectory.getPath(), destDir.getPath() ); log.debug( "Creating destination directory '{}' (current user '{}')", destDir.getName(), activePrincipal ); triggerAuditEvent( request.getRemoteAddr(), managedRepositoryContent.getId(), relPath, AuditEvent.CREATE_DIR, activePrincipal ); } } } return resource; } @Override public DavResource createResource( final DavResourceLocator locator, final DavSession davSession ) throws DavException { ArchivaDavResourceLocator archivaLocator = checkLocatorIsInstanceOfRepositoryLocator( locator ); ManagedRepositoryContent managedRepositoryContent; ManagedRepository repo = repositoryRegistry.getManagedRepository( archivaLocator.getRepositoryId( ) ); if (repo==null) { throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Invalid repository: " + archivaLocator.getRepositoryId() ); } managedRepositoryContent = repo.getContent(); if (managedRepositoryContent==null) { log.error("Inconsistency detected. Repository content not found for '{}'", archivaLocator.getRepositoryId()); throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Invalid repository: " + archivaLocator.getRepositoryId() ); } DavResource resource = null; String logicalResource = getLogicalResource( archivaLocator, repo, false ); if ( logicalResource.startsWith( "/" ) ) { logicalResource = logicalResource.substring( 1 ); } StorageAsset resourceAsset = repo.getAsset( logicalResource ); try { resource = new ArchivaDavResource( resourceAsset, logicalResource, repo, davSession, archivaLocator, this, mimeTypes, auditListeners, scheduler); } catch ( LayoutException e ) { log.error( "Incompatible layout: {}", e.getMessage( ), e ); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } resource.addLockManager( lockManager ); return resource; } private boolean fetchContentFromProxies( ManagedRepository managedRepository, DavServletRequest request, LogicalResource resource ) throws DavException { String path = resource.getPath(); if (!proxyRegistry.hasHandler(managedRepository.getType())) { throw new DavException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "No proxy handler found for repository type "+managedRepository.getType()); } RepositoryRequestInfo repositoryRequestInfo = managedRepository.getRequestInfo(); RepositoryProxyHandler proxyHandler = proxyRegistry.getHandler(managedRepository.getType()).get(0); if ( repositoryRequestInfo.isSupportFile( path ) ) { StorageAsset proxiedFile = proxyHandler.fetchFromProxies( managedRepository, path ); return ( proxiedFile != null ); } // Is it a Metadata resource? if ( "default".equals(repositoryRequestInfo.getLayout( path )) && repositoryRequestInfo.isMetadata( path ) ) { return proxyHandler.fetchMetadataFromProxies( managedRepository, path ).isModified(); } // Is it an Archetype Catalog? if ( repositoryRequestInfo.isArchetypeCatalog( path ) ) { // FIXME we must implement a merge of remote archetype catalog from remote servers. StorageAsset proxiedFile = proxyHandler.fetchFromProxies( managedRepository, path ); return ( proxiedFile != null ); } // Not any of the above? Then it's gotta be an artifact reference. try { // Get the artifact reference in a layout neutral way. // ArtifactReference artifact = repositoryRequestInfo.toArtifactReference( path ); ItemSelector selector = repositoryRequestInfo.toItemSelector( path ); if ( selector != null ) { String repositoryLayout = managedRepository.getLayout(); RepositoryStorage repositoryStorage = this.applicationContext.getBean( "repositoryStorage#" + repositoryLayout, RepositoryStorage.class ); selector = repositoryStorage.applyServerSideRelocation( managedRepository, selector ); StorageAsset proxiedFile = proxyHandler.fetchFromProxies( managedRepository, selector ); resource.setPath( managedRepository.getContent().toPath( selector ) ); log.debug( "Proxied artifact '{}:{}:{}:{}'", selector.getNamespace(), selector.getArtifactId(), selector.getVersion(), selector.getArtifactVersion() ); return ( proxiedFile != null ); } } catch ( LayoutException e ) { /* eat it */ } catch ( ProxyDownloadException e ) { log.error( e.getMessage(), e ); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to fetch artifact resource." ); } return false; } // TODO: remove? private void triggerAuditEvent( String remoteIP, String repositoryId, String resource, String action, String principal ) { AuditEvent event = new AuditEvent( repositoryId, principal, resource, action ); event.setRemoteIP( remoteIP ); for ( AuditListener listener : auditListeners ) { listener.auditEvent( event ); } } @Override public void addAuditListener( AuditListener listener ) { this.auditListeners.add( listener ); } @Override public void clearAuditListeners() { this.auditListeners.clear(); } @Override public void removeAuditListener( AuditListener listener ) { this.auditListeners.remove( listener ); } private void setHeaders( DavServletResponse response, DavResourceLocator locator, DavResource resource, boolean group ) { // [MRM-503] - Metadata file need Pragma:no-cache response // header. if ( locator.getResourcePath().endsWith( "/maven-metadata.xml" ) || ( resource instanceof ArchivaDavResource && ( ArchivaDavResource.class.cast( resource ).getAsset().isContainer() ) ) ) { response.setHeader( "Pragma", "no-cache" ); response.setHeader( "Cache-Control", "no-cache" ); response.setDateHeader( "Last-Modified", new Date().getTime() ); } // if the resource is a directory don't cache it as new groupId deployed will be available // without need of refreshing browser else if ( locator.getResourcePath().endsWith( "/maven-metadata.xml" ) || ( resource instanceof ArchivaVirtualDavResource && ( Files.isDirectory(Paths.get( ArchivaVirtualDavResource.class.cast( resource ).getLogicalResource() )) ) ) ) { response.setHeader( "Pragma", "no-cache" ); response.setHeader( "Cache-Control", "no-cache" ); response.setDateHeader( "Last-Modified", new Date().getTime() ); } else if ( group ) { if ( resource instanceof ArchivaVirtualDavResource ) { //MRM-1854 here we have a directory so force "Last-Modified" response.setDateHeader( "Last-Modified", new Date().getTime() ); } } else { // We need to specify this so connecting wagons can work correctly response.setDateHeader( "Last-Modified", resource.getModificationTime() ); } // TODO: [MRM-524] determine http caching options for other types of files (artifacts, sha1, md5, snapshots) } private ArchivaDavResourceLocator checkLocatorIsInstanceOfRepositoryLocator( DavResourceLocator locator ) throws DavException { if ( !( locator instanceof ArchivaDavResourceLocator ) ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Locator does not implement RepositoryLocator" ); } // Hidden paths if ( locator.getResourcePath().startsWith( ArchivaDavResource.HIDDEN_PATH_PREFIX ) ) { throw new DavException( HttpServletResponse.SC_NOT_FOUND ); } ArchivaDavResourceLocator archivaLocator = (ArchivaDavResourceLocator) locator; // MRM-419 - Windows Webdav support. Should not 404 if there is no content. if ( StringUtils.isEmpty( archivaLocator.getRepositoryId() ) ) { throw new DavException( HttpServletResponse.SC_NO_CONTENT ); } return archivaLocator; } private String addHrefPrefix( String contextPath, String path ) { String prefix = archivaConfiguration.getConfiguration().getWebapp().getUi().getApplicationUrl(); if (prefix == null || prefix.isEmpty()) { prefix = contextPath; } return prefix + ( StringUtils.startsWith( path, "/" ) ? "" : ( StringUtils.endsWith( prefix, "/" ) ? "" : "/" ) ) + path; } public void setProxyRegistry(ProxyRegistry proxyRegistry) { this.proxyRegistry = proxyRegistry; } public ProxyRegistry getProxyRegistry() { return this.proxyRegistry; } private static class LogicalResource { private String path; public LogicalResource( String path ) { this.path = path; } public String getPath() { return path; } public void setPath( String path ) { this.path = path; } } protected boolean isAuthorized( DavServletRequest request, String repositoryId ) throws DavException { try { AuthenticationResult result = httpAuth.getAuthenticationResult( request, null ); SecuritySession securitySession = httpAuth.getSecuritySession( request.getSession( true ) ); return servletAuth.isAuthenticated( request, result ) // && servletAuth.isAuthorized( request, securitySession, repositoryId, // WebdavMethodUtil.getMethodPermission( request.getMethod() ) ); } catch ( AuthenticationException e ) { // safety check for MRM-911 String guest = UserManager.GUEST_USERNAME; try { if ( servletAuth.isAuthorized( guest, ( (ArchivaDavResourceLocator) request.getRequestLocator() ).getRepositoryId(), WebdavMethodUtil.getMethodPermission( request.getMethod() ) ) ) { return true; } } catch ( UnauthorizedException ae ) { throw new UnauthorizedDavException( repositoryId, "You are not authenticated and authorized to access any repository." ); } throw new UnauthorizedDavException( repositoryId, "You are not authenticated" ); } catch ( MustChangePasswordException e ) { throw new UnauthorizedDavException( repositoryId, "You must change your password." ); } catch ( AccountLockedException e ) { throw new UnauthorizedDavException( repositoryId, "User account is locked." ); } catch ( AuthorizationException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Fatal Authorization Subsystem Error." ); } catch ( UnauthorizedException e ) { throw new UnauthorizedDavException( repositoryId, e.getMessage() ); } } private DavResource getResourceFromGroup( DavServletRequest request, ArchivaDavResourceLocator locator, RepositoryGroup repositoryGroup ) throws DavException { final String id = repositoryGroup.getId(); final List<ManagedRepository> repositories = repositoryGroup.getRepositories(); if ( repositories == null || repositories.isEmpty() ) { try { return new ArchivaDavResource( repositoryGroup.getAsset("/"), "groups/" + id, null, request.getDavSession(), locator, this, mimeTypes, auditListeners, scheduler); } catch (LayoutException e) { log.error("Bad repository layout: {}", e.getMessage(), e); throw new DavException(500, e); } } List<StorageAsset> mergedRepositoryContents = new ArrayList<>(); ManagedRepository firstRepo = repositories.get( 0 ); String path = getLogicalResource( locator, firstRepo, false ); if ( path.startsWith( "/" ) ) { path = path.substring( 1 ); } LogicalResource logicalResource = new LogicalResource( path ); // flow: // if the current user logged in has permission to any of the repositories, allow user to // browse the repo group but displaying only the repositories which the user has permission to access. // otherwise, prompt for authentication. String activePrincipal = getActivePrincipal( request ); boolean allow = isAllowedToContinue( request, repositories, activePrincipal ); // remove last / String pathInfo = StringUtils.removeEnd( request.getPathInfo(), "/" ); String mergedIndexPath = "/"; if (repositoryGroup.supportsFeature( IndexCreationFeature.class )) { IndexCreationFeature indexCreationFeature = repositoryGroup.getFeature( IndexCreationFeature.class ).get(); mergedIndexPath = indexCreationFeature.getIndexPath().getPath(); } if ( allow ) { if ( StringUtils.endsWith( pathInfo, mergedIndexPath ) ) { StorageAsset mergedRepoDirPath = buildMergedIndexDirectory( activePrincipal, request, repositoryGroup ); mergedRepositoryContents.add( mergedRepoDirPath ); } else { if ( StringUtils.equalsIgnoreCase( pathInfo, "/" + id ) ) { Path tmpDirectory = Paths.get( SystemUtils.getJavaIoTmpDir().toString(), id, mergedIndexPath ); if ( !Files.exists(tmpDirectory) ) { synchronized ( tmpDirectory.toAbsolutePath().toString() ) { if ( !Files.exists(tmpDirectory) ) { try { Files.createDirectories( tmpDirectory ); } catch ( IOException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not create direcotory "+tmpDirectory ); } } } } try { FilesystemStorage storage = new FilesystemStorage(tmpDirectory.getParent(), new DefaultFileLockManager()); mergedRepositoryContents.add( storage.getRoot() ); } catch (IOException e) { throw new DavException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Could not create storage for " + tmpDirectory); } } for ( ManagedRepository repo : repositories ) { ManagedRepositoryContent managedRepository = null; if (repo == null) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Invalid managed repository <" + repo.getId() + ">"); } managedRepository = repo.getContent(); if (managedRepository==null) { log.error("Inconsistency detected. Repository content not found for '{}'",repo.getId()); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Invalid managed repository <" + repo.getId() + ">"); } // Path resourceFile = Paths.get( managedRepository.getRepoRoot(), logicalResource.getPath() ); StorageAsset resourceFile = repo.getAsset(logicalResource.getPath()); if ( resourceFile.exists() && managedRepository.getRepository().supportsFeature( IndexCreationFeature.class )) { // in case of group displaying index directory doesn't have sense !! IndexCreationFeature idf = managedRepository.getRepository().getFeature(IndexCreationFeature.class).get(); StorageAsset repoIndexDirectory = idf.getLocalIndexPath(); if ( !StringUtils.equals( FilenameUtils.normalize( repoIndexDirectory.getPath() ), FilenameUtils.normalize( logicalResource.getPath() ) ) ) { // for prompted authentication if ( httpAuth.getSecuritySession( request.getSession( true ) ) != null ) { try { if ( isAuthorized( request, repo.getId() ) ) { mergedRepositoryContents.add( resourceFile ); log.debug( "Repository '{}' accessed by '{}'", repo.getId(), activePrincipal ); } } catch ( DavException e ) { // TODO: review exception handling log.debug( "Skipping repository '{}' for user '{}': {}", managedRepository, activePrincipal, e.getMessage() ); } } else { // for the current user logged in try { if ( servletAuth.isAuthorized( activePrincipal, repo.getId(), WebdavMethodUtil.getMethodPermission( request.getMethod() ) ) ) { mergedRepositoryContents.add( resourceFile ); log.debug( "Repository '{}' accessed by '{}'", repo.getId(), activePrincipal ); } } catch ( UnauthorizedException e ) { // TODO: review exception handling log.debug( "Skipping repository '{}' for user '{}': {}", managedRepository, activePrincipal, e.getMessage() ); } } } } } } } else { throw new UnauthorizedDavException( locator.getRepositoryId(), "User not authorized." ); } ArchivaVirtualDavResource resource = new ArchivaVirtualDavResource( mergedRepositoryContents, logicalResource.getPath(), mimeTypes, locator, this ); // compatibility with MRM-440 to ensure browsing the repository group works ok if ( resource.isCollection() && !request.getRequestURI().endsWith( "/" ) ) { throw new BrowserRedirectException( resource.getHref() ); } return resource; } protected String getActivePrincipal( DavServletRequest request ) { User sessionUser = httpAuth.getSessionUser( request.getSession() ); return sessionUser != null ? sessionUser.getUsername() : UserManager.GUEST_USERNAME; } /** * Check if the current user is authorized to access any of the repos * * @param request * @param repositories * @param activePrincipal * @return */ private boolean isAllowedToContinue( DavServletRequest request, List<ManagedRepository> repositories, String activePrincipal ) { // when no repositories configured it's impossible to browse nothing ! // at least make possible to see nothing :-) if ( repositories == null || repositories.isEmpty() ) { return true; } boolean allow = false; // if securitySession != null, it means that the user was prompted for authentication if ( httpAuth.getSecuritySession( request.getSession() ) != null ) { for ( ManagedRepository repository : repositories ) { try { if ( isAuthorized( request, repository.getId() ) ) { allow = true; break; } } catch ( DavException e ) { continue; } } } else { for ( ManagedRepository repository : repositories ) { try { if ( servletAuth.isAuthorized( activePrincipal, repository.getId(), WebdavMethodUtil.getMethodPermission( request.getMethod() ) ) ) { allow = true; break; } } catch ( UnauthorizedException e ) { continue; } } } return allow; } private StorageAsset writeMergedMetadataToFile( RepositoryGroup repoGroup, ArchivaRepositoryMetadata mergedMetadata, String outputFilename ) throws RepositoryMetadataException, IOException { StorageAsset asset = repoGroup.addAsset( outputFilename, false ); OutputStream stream = asset.getWriteStream( true ); OutputStreamWriter sw = new OutputStreamWriter( stream, "UTF-8" ); RepositoryMetadataWriter.write( mergedMetadata, sw ); createChecksumFiles( repoGroup, outputFilename ); return asset; } private void createChecksumFiles(RepositoryGroup repo, String path) { List<ChecksumAlgorithm> algorithms = ChecksumUtil.getAlgorithms( archivaConfiguration.getConfiguration( ).getArchivaRuntimeConfiguration( ).getChecksumTypes( ) ); List<OutputStream> outStreams = algorithms.stream( ).map( algo -> { String ext = algo.getDefaultExtension( ); try { return repo.getAsset( path + "." + ext ).getWriteStream( true ); } catch ( IOException e ) { e.printStackTrace( ); return null; } } ).filter( Objects::nonNull ).collect( Collectors.toList( ) ); try { StreamingChecksum.updateChecksums( repo.getAsset(path).getReadStream(), algorithms, outStreams ); } catch ( IOException e ) { e.printStackTrace( ); } } private boolean isProjectReference( String requestedResource ) { try { metadataTools.toVersionedSelector( requestedResource ); return false; } catch ( RepositoryMetadataException re ) { return true; } } protected StorageAsset buildMergedIndexDirectory( String activePrincipal, DavServletRequest request, RepositoryGroup repositoryGroup ) throws DavException { try { final List<ManagedRepository> repositories = repositoryGroup.getRepositories(); HttpSession session = request.getSession(); @SuppressWarnings( "unchecked" ) Map<String, TemporaryGroupIndex> temporaryGroupIndexMap = (Map<String, TemporaryGroupIndex>) session.getAttribute( TemporaryGroupIndexSessionCleaner.TEMPORARY_INDEX_SESSION_KEY ); if ( temporaryGroupIndexMap == null ) { temporaryGroupIndexMap = new HashMap<>(); } final String id = repositoryGroup.getId(); TemporaryGroupIndex tmp = temporaryGroupIndexMap.get(id); if ( tmp != null && tmp.getDirectory() != null && tmp.getDirectory().exists()) { if ( System.currentTimeMillis() - tmp.getCreationTime() > ( repositoryGroup.getMergedIndexTTL() * 60 * 1000 ) ) { log.debug( MarkerFactory.getMarker( "group.merged.index" ), "tmp group index '{}' is too old so delete it", id); indexMerger.cleanTemporaryGroupIndex( tmp ); } else { log.debug( MarkerFactory.getMarker( "group.merged.index" ), "merged index for group '{}' found in cache", id); return tmp.getDirectory(); } } Set<String> authzRepos = new HashSet<String>(); String permission = WebdavMethodUtil.getMethodPermission( request.getMethod() ); for ( ManagedRepository repository : repositories ) { try { if ( servletAuth.isAuthorized( activePrincipal, repository.getId(), permission ) ) { authzRepos.add( repository.getId() ); authzRepos.addAll( this.repositorySearch.getRemoteIndexingContextIds( repository.getId() ) ); } } catch ( UnauthorizedException e ) { // TODO: review exception handling log.debug( "Skipping repository '{}' for user '{}': {}", repository, activePrincipal, e.getMessage() ); } } log.info( "generate temporary merged index for repository group '{}' for repositories '{}'", id, authzRepos ); IndexCreationFeature indexCreationFeature = repositoryGroup.getFeature( IndexCreationFeature.class ).get(); Path indexPath = indexCreationFeature.getLocalIndexPath().getFilePath(); if (indexPath!=null) { Path tempRepoFile = Files.createTempDirectory( "temp" ); tempRepoFile.toFile( ).deleteOnExit( ); FilesystemStorage storage = new FilesystemStorage(tempRepoFile, new DefaultFileLockManager()); StorageAsset tmpAsset = storage.getRoot(); IndexMergerRequest indexMergerRequest = new IndexMergerRequest( authzRepos, true, id, indexPath.toString( ), repositoryGroup.getMergedIndexTTL( ) ).mergedIndexDirectory( tmpAsset ).temporary( true ); MergedRemoteIndexesTaskRequest taskRequest = new MergedRemoteIndexesTaskRequest( indexMergerRequest, indexMerger ); MergedRemoteIndexesTask job = new MergedRemoteIndexesTask( taskRequest ); ArchivaIndexingContext indexingContext = job.execute( ).getIndexingContext( ); StorageAsset mergedRepoDir = indexingContext.getPath( ); TemporaryGroupIndex temporaryGroupIndex = new TemporaryGroupIndex( mergedRepoDir, indexingContext.getId( ), id, repositoryGroup.getMergedIndexTTL( ) ) // .setCreationTime( new Date( ).getTime( ) ); temporaryGroupIndexMap.put( id, temporaryGroupIndex ); session.setAttribute( TemporaryGroupIndexSessionCleaner.TEMPORARY_INDEX_SESSION_KEY, temporaryGroupIndexMap ); return mergedRepoDir; } else { log.error("Local index path for repository group {} does not exist.", repositoryGroup.getId()); throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR ); } } catch ( RepositorySearchException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } catch ( IndexMergerException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } catch ( IOException e ) { throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e ); } } public void setServletAuth( ServletAuthenticator servletAuth ) { this.servletAuth = servletAuth; } public void setHttpAuth( HttpAuthenticator httpAuth ) { this.httpAuth = httpAuth; } public void setScheduler( RepositoryArchivaTaskScheduler scheduler ) { this.scheduler = scheduler; } public void setArchivaConfiguration( ArchivaConfiguration archivaConfiguration ) { this.archivaConfiguration = archivaConfiguration; } public RemoteRepositoryAdmin getRemoteRepositoryAdmin() { return remoteRepositoryAdmin; } public void setRemoteRepositoryAdmin( RemoteRepositoryAdmin remoteRepositoryAdmin ) { this.remoteRepositoryAdmin = remoteRepositoryAdmin; } public ManagedRepositoryAdmin getManagedRepositoryAdmin() { return managedRepositoryAdmin; } public void setManagedRepositoryAdmin( ManagedRepositoryAdmin managedRepositoryAdmin ) { this.managedRepositoryAdmin = managedRepositoryAdmin; } public RepositoryRegistry getRepositoryRegistry( ) { return repositoryRegistry; } public void setRepositoryRegistry( RepositoryRegistry repositoryRegistry ) { this.repositoryRegistry = repositoryRegistry; } }