/* This file is part of Airsonic. Airsonic is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Airsonic is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Airsonic. If not, see . Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.security; import org.airsonic.player.controller.JAXBWriter; import org.airsonic.player.controller.SubsonicRESTController; import org.airsonic.player.domain.User; import org.airsonic.player.domain.Version; import org.airsonic.player.service.SecurityService; import org.airsonic.player.util.StringUtil; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.util.matcher.RegexRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * Performs authentication based on credentials being present in the HTTP request parameters. Also checks * API versions and license information. *

* The username should be set in parameter "u", and the password should be set in parameter "p". * The REST protocol version should be set in parameter "v". *

* The password can either be in plain text or be UTF-8 hexencoded preceded by "enc:". * * @author Sindre Mehus */ public class RESTRequestParameterProcessingFilter implements Filter { private static final Logger LOG = LoggerFactory.getLogger(RESTRequestParameterProcessingFilter.class); private final JAXBWriter jaxbWriter = new JAXBWriter(); private AuthenticationManager authenticationManager; private SecurityService securityService; private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource(); private ApplicationEventPublisher eventPublisher; private static RequestMatcher requiresAuthenticationRequestMatcher = new RegexRequestMatcher("/rest/.+",null); protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return requiresAuthenticationRequestMatcher.matches(request); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!(request instanceof HttpServletRequest)) { throw new ServletException("Can only process HttpServletRequest"); } if (!(response instanceof HttpServletResponse)) { throw new ServletException("Can only process HttpServletResponse"); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; if (!requiresAuthentication(httpRequest, httpResponse)) { chain.doFilter(request, response); return; } String username = StringUtils.trimToNull(httpRequest.getParameter("u")); String password = decrypt(StringUtils.trimToNull(httpRequest.getParameter("p"))); String salt = StringUtils.trimToNull(httpRequest.getParameter("s")); String token = StringUtils.trimToNull(httpRequest.getParameter("t")); String version = StringUtils.trimToNull(httpRequest.getParameter("v")); String client = StringUtils.trimToNull(httpRequest.getParameter("c")); SubsonicRESTController.ErrorCode errorCode = null; // The username and credentials parameters are not required if the user // was previously authenticated, for example using Basic Auth. boolean passwordOrTokenPresent = password != null || (salt != null && token != null); Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication(); boolean missingCredentials = previousAuth == null && (username == null || !passwordOrTokenPresent); if (missingCredentials || version == null || client == null) { errorCode = SubsonicRESTController.ErrorCode.MISSING_PARAMETER; } if (errorCode == null) { errorCode = checkAPIVersion(version); } if (errorCode == null) { errorCode = authenticate(httpRequest, username, password, salt, token, previousAuth); } if (errorCode == null) { chain.doFilter(request, response); } else { SecurityContextHolder.getContext().setAuthentication(null); sendErrorXml(httpRequest, httpResponse, errorCode); } } private SubsonicRESTController.ErrorCode checkAPIVersion(String version) { Version serverVersion = new Version(jaxbWriter.getRestProtocolVersion()); Version clientVersion = new Version(version); if (serverVersion.getMajor() > clientVersion.getMajor()) { return SubsonicRESTController.ErrorCode.PROTOCOL_MISMATCH_CLIENT_TOO_OLD; } else if (serverVersion.getMajor() < clientVersion.getMajor()) { return SubsonicRESTController.ErrorCode.PROTOCOL_MISMATCH_SERVER_TOO_OLD; } else if (serverVersion.getMinor() < clientVersion.getMinor()) { return SubsonicRESTController.ErrorCode.PROTOCOL_MISMATCH_SERVER_TOO_OLD; } return null; } private SubsonicRESTController.ErrorCode authenticate(HttpServletRequest httpRequest, String username, String password, String salt, String token, Authentication previousAuth) { // Previously authenticated and username not overridden? if (username == null && previousAuth != null) { return null; } if (salt != null && token != null) { User user = securityService.getUserByName(username); if (user == null) { return SubsonicRESTController.ErrorCode.NOT_AUTHENTICATED; } String expectedToken = DigestUtils.md5Hex(user.getPassword() + salt); if (!expectedToken.equals(token)) { return SubsonicRESTController.ErrorCode.NOT_AUTHENTICATED; } password = user.getPassword(); } if (password != null) { UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); authRequest.setDetails(authenticationDetailsSource.buildDetails(httpRequest)); try { Authentication authResult = authenticationManager.authenticate(authRequest); SecurityContextHolder.getContext().setAuthentication(authResult); return null; } catch (AuthenticationException x) { eventPublisher.publishEvent(new AuthenticationFailureBadCredentialsEvent(authRequest, x)); return SubsonicRESTController.ErrorCode.NOT_AUTHENTICATED; } } return SubsonicRESTController.ErrorCode.MISSING_PARAMETER; } public static String decrypt(String s) { if (s == null) { return null; } if (!s.startsWith("enc:")) { return s; } try { return StringUtil.utf8HexDecode(s.substring(4)); } catch (Exception e) { return s; } } private void sendErrorXml(HttpServletRequest request, HttpServletResponse response, SubsonicRESTController.ErrorCode errorCode) { try { jaxbWriter.writeErrorResponse(request, response, errorCode, errorCode.getMessage()); } catch (Exception e) { LOG.error("Failed to send error response.", e); } } public void init(FilterConfig filterConfig) { } public void destroy() { } public void setAuthenticationManager(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } public SecurityService getSecurityService() { return securityService; } public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } public void setEventPublisher(ApplicationEventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } }