001package org.apache.archiva.web.rss; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import com.sun.syndication.feed.synd.SyndFeed; 023import com.sun.syndication.io.FeedException; 024import com.sun.syndication.io.SyndFeedOutput; 025import org.apache.archiva.metadata.repository.RepositorySession; 026import org.apache.archiva.metadata.repository.RepositorySessionFactory; 027import org.apache.archiva.redback.authentication.AuthenticationException; 028import org.apache.archiva.redback.authentication.AuthenticationResult; 029import org.apache.archiva.redback.authorization.AuthorizationException; 030import org.apache.archiva.redback.authorization.UnauthorizedException; 031import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator; 032import org.apache.archiva.redback.policy.AccountLockedException; 033import org.apache.archiva.redback.policy.MustChangePasswordException; 034import org.apache.archiva.redback.system.SecuritySession; 035import org.apache.archiva.redback.users.UserManager; 036import org.apache.archiva.redback.users.UserNotFoundException; 037import org.apache.archiva.rss.processor.RssFeedProcessor; 038import org.apache.archiva.security.AccessDeniedException; 039import org.apache.archiva.security.ArchivaSecurityException; 040import org.apache.archiva.security.PrincipalNotFoundException; 041import org.apache.archiva.security.ServletAuthenticator; 042import org.apache.archiva.security.UserRepositories; 043import org.apache.archiva.security.common.ArchivaRoleConstants; 044import org.apache.commons.codec.Decoder; 045import org.apache.commons.codec.DecoderException; 046import org.apache.commons.codec.binary.Base64; 047import org.apache.commons.lang.StringUtils; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050import org.springframework.web.context.WebApplicationContext; 051import org.springframework.web.context.support.WebApplicationContextUtils; 052 053import javax.servlet.ServletConfig; 054import javax.servlet.ServletException; 055import javax.servlet.http.HttpServlet; 056import javax.servlet.http.HttpServletRequest; 057import javax.servlet.http.HttpServletResponse; 058import java.io.IOException; 059import java.util.ArrayList; 060import java.util.Collections; 061import java.util.HashMap; 062import java.util.List; 063import java.util.Map; 064 065/** 066 * Servlet for handling rss feed requests. 067 */ 068public class RssFeedServlet 069 extends HttpServlet 070{ 071 public static final String MIME_TYPE = "application/rss+xml; charset=UTF-8"; 072 073 private static final String COULD_NOT_GENERATE_FEED_ERROR = "Could not generate feed"; 074 075 private static final String COULD_NOT_AUTHENTICATE_USER = "Could not authenticate user"; 076 077 private static final String USER_NOT_AUTHORIZED = "User not authorized to access feed."; 078 079 private Logger log = LoggerFactory.getLogger( RssFeedServlet.class ); 080 081 private WebApplicationContext wac; 082 083 private UserRepositories userRepositories; 084 085 private ServletAuthenticator servletAuth; 086 087 private HttpAuthenticator httpAuth; 088 089 private RssFeedProcessor newArtifactsprocessor; 090 091 private RssFeedProcessor newVersionsprocessor; 092 093 /** 094 * FIXME: this could be multiple implementations and needs to be configured. 095 */ 096 private RepositorySessionFactory repositorySessionFactory; 097 098 @Override 099 public void init( ServletConfig servletConfig ) 100 throws ServletException 101 { 102 super.init( servletConfig ); 103 wac = WebApplicationContextUtils.getRequiredWebApplicationContext( servletConfig.getServletContext() ); 104 userRepositories = wac.getBean( UserRepositories.class ); 105 servletAuth = wac.getBean( ServletAuthenticator.class ); 106 httpAuth = wac.getBean( "httpAuthenticator#basic", HttpAuthenticator.class ); 107 // TODO: what if there are other types? 108 repositorySessionFactory = wac.getBean( "repositorySessionFactory", RepositorySessionFactory.class ); 109 110 newArtifactsprocessor = wac.getBean( "rssFeedProcessor#new-artifacts", RssFeedProcessor.class ); 111 newVersionsprocessor = wac.getBean( "rssFeedProcessor#new-versions", RssFeedProcessor.class ); 112 } 113 114 @Override 115 public void doGet( HttpServletRequest req, HttpServletResponse res ) 116 throws ServletException, IOException 117 { 118 119 120 String repoId = null; 121 String groupId = null; 122 String artifactId = null; 123 124 String url = StringUtils.removeEnd( req.getRequestURL().toString(), "/" ); 125 126 if ( StringUtils.countMatches( StringUtils.substringAfter( url, "feeds/" ), "/" ) > 0 ) 127 { 128 artifactId = StringUtils.substringAfterLast( url, "/" ); 129 groupId = StringUtils.substringBeforeLast( StringUtils.substringAfter( url, "feeds/" ), "/" ); 130 groupId = StringUtils.replaceChars( groupId, '/', '.' ); 131 } 132 else if ( StringUtils.countMatches( StringUtils.substringAfter( url, "feeds/" ), "/" ) == 0 ) 133 { 134 // we receive feeds?babla=ded which is not correct 135 if ( StringUtils.countMatches( url, "feeds?" ) > 0 ) 136 { 137 res.sendError( HttpServletResponse.SC_BAD_REQUEST, "Invalid request url." ); 138 return; 139 } 140 repoId = StringUtils.substringAfterLast( url, "/" ); 141 } 142 else 143 { 144 res.sendError( HttpServletResponse.SC_BAD_REQUEST, "Invalid request url." ); 145 return; 146 } 147 148 RssFeedProcessor processor = null; 149 150 try 151 { 152 Map<String, String> map = new HashMap<>(); 153 SyndFeed feed = null; 154 155 if ( isAllowed( req, repoId, groupId, artifactId ) ) 156 { 157 if ( repoId != null ) 158 { 159 // new artifacts in repo feed request 160 processor = newArtifactsprocessor; 161 map.put( RssFeedProcessor.KEY_REPO_ID, repoId ); 162 } 163 else if ( ( groupId != null ) && ( artifactId != null ) ) 164 { 165 // TODO: this only works for guest - we could pass in the list of repos 166 // new versions of artifact feed request 167 processor = newVersionsprocessor; 168 map.put( RssFeedProcessor.KEY_GROUP_ID, groupId ); 169 map.put( RssFeedProcessor.KEY_ARTIFACT_ID, artifactId ); 170 } 171 } 172 else 173 { 174 res.sendError( HttpServletResponse.SC_UNAUTHORIZED, USER_NOT_AUTHORIZED ); 175 return; 176 } 177 178 RepositorySession repositorySession = repositorySessionFactory.createSession(); 179 try 180 { 181 feed = processor.process( map, repositorySession.getRepository() ); 182 } 183 finally 184 { 185 repositorySession.close(); 186 } 187 if ( feed == null ) 188 { 189 res.sendError( HttpServletResponse.SC_NO_CONTENT, "No information available." ); 190 return; 191 } 192 193 res.setContentType( MIME_TYPE ); 194 195 if ( repoId != null ) 196 { 197 feed.setLink( req.getRequestURL().toString() ); 198 } 199 else if ( ( groupId != null ) && ( artifactId != null ) ) 200 { 201 feed.setLink( req.getRequestURL().toString() ); 202 } 203 204 SyndFeedOutput output = new SyndFeedOutput(); 205 output.output( feed, res.getWriter() ); 206 } 207 catch ( UserNotFoundException unfe ) 208 { 209 log.debug( COULD_NOT_AUTHENTICATE_USER, unfe ); 210 res.sendError( HttpServletResponse.SC_UNAUTHORIZED, COULD_NOT_AUTHENTICATE_USER ); 211 } 212 catch ( AccountLockedException acce ) 213 { 214 res.sendError( HttpServletResponse.SC_UNAUTHORIZED, COULD_NOT_AUTHENTICATE_USER ); 215 } 216 catch ( AuthenticationException authe ) 217 { 218 log.debug( COULD_NOT_AUTHENTICATE_USER, authe ); 219 res.sendError( HttpServletResponse.SC_UNAUTHORIZED, COULD_NOT_AUTHENTICATE_USER ); 220 } 221 catch ( FeedException ex ) 222 { 223 log.debug( COULD_NOT_GENERATE_FEED_ERROR, ex ); 224 res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, COULD_NOT_GENERATE_FEED_ERROR ); 225 } 226 catch ( MustChangePasswordException e ) 227 { 228 res.sendError( HttpServletResponse.SC_UNAUTHORIZED, COULD_NOT_AUTHENTICATE_USER ); 229 } 230 catch ( UnauthorizedException e ) 231 { 232 log.debug( e.getMessage() ); 233 if ( repoId != null ) 234 { 235 res.setHeader( "WWW-Authenticate", 236 "Basic realm=\"Repository Archiva Managed " + repoId + " Repository" ); 237 } 238 else 239 { 240 res.setHeader( "WWW-Authenticate", "Basic realm=\"Artifact " + groupId + ":" + artifactId ); 241 } 242 243 res.sendError( HttpServletResponse.SC_UNAUTHORIZED, USER_NOT_AUTHORIZED ); 244 } 245 } 246 247 /** 248 * Basic authentication. 249 * 250 * @param req 251 * @param repositoryId TODO 252 * @param groupId TODO 253 * @param artifactId TODO 254 * @return 255 */ 256 private boolean isAllowed( HttpServletRequest req, String repositoryId, String groupId, String artifactId ) 257 throws UserNotFoundException, AccountLockedException, AuthenticationException, MustChangePasswordException, 258 UnauthorizedException 259 { 260 String auth = req.getHeader( "Authorization" ); 261 List<String> repoIds = new ArrayList<>(); 262 263 if ( repositoryId != null ) 264 { 265 repoIds.add( repositoryId ); 266 } 267 else if ( artifactId != null && groupId != null ) 268 { 269 if ( auth != null ) 270 { 271 if ( !auth.toUpperCase().startsWith( "BASIC " ) ) 272 { 273 return false; 274 } 275 276 Decoder dec = new Base64(); 277 String usernamePassword = ""; 278 279 try 280 { 281 usernamePassword = new String( (byte[]) dec.decode( auth.substring( 6 ).getBytes() ) ); 282 } 283 catch ( DecoderException ie ) 284 { 285 log.warn( "Error decoding username and password: {}", ie.getMessage() ); 286 } 287 288 if ( usernamePassword == null || usernamePassword.trim().equals( "" ) ) 289 { 290 repoIds = getObservableRepos( UserManager.GUEST_USERNAME ); 291 } 292 else 293 { 294 String[] userCredentials = usernamePassword.split( ":" ); 295 repoIds = getObservableRepos( userCredentials[0] ); 296 } 297 } 298 else 299 { 300 repoIds = getObservableRepos( UserManager.GUEST_USERNAME ); 301 } 302 } 303 else 304 { 305 return false; 306 } 307 308 for ( String repoId : repoIds ) 309 { 310 try 311 { 312 AuthenticationResult result = httpAuth.getAuthenticationResult( req, null ); 313 SecuritySession securitySession = httpAuth.getSecuritySession( req.getSession( true ) ); 314 315 if ( servletAuth.isAuthenticated( req, result ) // 316 && servletAuth.isAuthorized( req, // 317 securitySession, // 318 repoId, // 319 ArchivaRoleConstants.OPERATION_REPOSITORY_ACCESS ) ) 320 { 321 return true; 322 } 323 } 324 catch ( AuthorizationException e ) 325 { 326 log.debug( "AuthorizationException for repoId: {}", repoId ); 327 } 328 catch ( UnauthorizedException e ) 329 { 330 log.debug( "UnauthorizedException for repoId: {}", repoId ); 331 } 332 } 333 334 throw new UnauthorizedException( "Access denied." ); 335 } 336 337 private List<String> getObservableRepos( String principal ) 338 { 339 try 340 { 341 return userRepositories.getObservableRepositoryIds( principal ); 342 } 343 catch ( PrincipalNotFoundException e ) 344 { 345 log.warn( e.getMessage(), e ); 346 } 347 catch ( AccessDeniedException e ) 348 { 349 log.warn( e.getMessage(), e ); 350 } 351 catch ( ArchivaSecurityException e ) 352 { 353 log.warn( e.getMessage(), e ); 354 } 355 356 return Collections.emptyList(); 357 } 358 359}