diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/HLSController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/HLSController.java index c70f10a8c..1de42cbf8 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/HLSController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/HLSController.java @@ -68,9 +68,6 @@ public class HLSController { @GetMapping public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { - - response.setHeader("Access-Control-Allow-Origin", "*"); - int id = ServletRequestUtils.getIntParameter(request, "id", 0); MediaFile mediaFile = mediaFileService.getMediaFile(id); Player player = playerService.getPlayer(request, response); diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java index e5ae6deed..e55cf24f8 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/StreamController.java @@ -109,8 +109,6 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon LOG.info("{}: Incoming Podcast request for playlist {}", request.getRemoteAddr(), playlistId); } - response.setHeader("Access-Control-Allow-Origin", "*"); - String contentType = StringUtil.getMimeType(request.getParameter("suffix")); response.setContentType(contentType); diff --git a/airsonic-main/src/main/java/org/airsonic/player/filter/RESTFilter.java b/airsonic-main/src/main/java/org/airsonic/player/filter/RESTFilter.java index 686f079e0..6011d456b 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/filter/RESTFilter.java +++ b/airsonic-main/src/main/java/org/airsonic/player/filter/RESTFilter.java @@ -33,8 +33,6 @@ /** * Intercepts exceptions thrown by RESTController. * - * Also adds the CORS response header (http://enable-cors.org) - * * @author Sindre Mehus * @version $Revision: 1.1 $ $Date: 2006/03/01 16:58:08 $ */ @@ -46,8 +44,6 @@ public class RESTFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { try { - HttpServletResponse response = (HttpServletResponse) res; - response.setHeader("Access-Control-Allow-Origin", "*"); chain.doFilter(req, res); } catch (Throwable x) { handleException(x, (HttpServletRequest) req, (HttpServletResponse) res); diff --git a/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java b/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java index 5e84d2cd0..e91d0b34a 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java +++ b/airsonic-main/src/main/java/org/airsonic/player/security/GlobalSecurityConfig.java @@ -24,8 +24,12 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.security.SecureRandom; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -151,12 +155,10 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { - RESTRequestParameterProcessingFilter restAuthenticationFilter = new RESTRequestParameterProcessingFilter(); restAuthenticationFilter.setAuthenticationManager(authenticationManagerBean()); restAuthenticationFilter.setSecurityService(securityService); restAuthenticationFilter.setEventPublisher(eventPublisher); - http = http.addFilterBefore(restAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // Try to load the 'remember me' key. // @@ -185,6 +187,9 @@ protected void configure(HttpSecurity http) throws Exception { } http + .cors() + .and() + .addFilterBefore(restAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .csrf() .requireCsrfProtectionMatcher(csrfSecurityRequestMatcher) .and().headers() @@ -232,4 +237,18 @@ protected void configure(HttpSecurity http) throws Exception { } } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Collections.singletonList("*")); + configuration.setAllowedMethods(Collections.singletonList("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/rest/**", configuration); + source.registerCorsConfiguration("/stream/**", configuration); + source.registerCorsConfiguration("/ext/stream/**", configuration); + source.registerCorsConfiguration("/ext/hls/**", configuration); + source.registerCorsConfiguration("/ext/hls/**", configuration); + return source; + } } diff --git a/airsonic-main/src/test/java/org/airsonic/player/api/CORSTest.java b/airsonic-main/src/test/java/org/airsonic/player/api/CORSTest.java new file mode 100644 index 000000000..27d29de15 --- /dev/null +++ b/airsonic-main/src/test/java/org/airsonic/player/api/CORSTest.java @@ -0,0 +1,103 @@ +package org.airsonic.player.api; + +import org.airsonic.player.TestCaseUtils; +import org.airsonic.player.util.HomeRule; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class CORSTest { + + private static final String CLIENT_NAME = "airsonic"; + private static final String AIRSONIC_USER = "admin"; + private static final String AIRSONIC_PASSWORD = "admin"; + private static final String EXPECTED_FORMAT = "json"; + private static String AIRSONIC_API_VERSION; + + @Autowired + private WebApplicationContext wac; + + private MockMvc mvc; + + @Before + public void setup() { + AIRSONIC_API_VERSION = TestCaseUtils.restApiVersion(); + mvc = MockMvcBuilders + .webAppContextSetup(wac) + .apply(springSecurity()) + .dispatchOptions(true) + .alwaysDo(print()) + .build(); + } + + @ClassRule + public static final HomeRule classRule = new HomeRule(); + + @Test + public void corsHeadersShouldBeAddedToSuccessResponses() throws Exception { + mvc.perform(get("/rest/ping") + .param("v", AIRSONIC_API_VERSION) + .param("c", CLIENT_NAME) + .param("u", AIRSONIC_USER) + .param("p", AIRSONIC_PASSWORD) + .param("f", EXPECTED_FORMAT) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.subsonic-response.status").value("ok")); + } + + @Test + public void corsHeadersShouldBeAddedToErrorResponses() throws Exception { + mvc.perform(get("/rest/ping") + .header("Access-Control-Request-Method", "GET") + .header("Origin", "https://example.com") + .param("v", AIRSONIC_API_VERSION) + .param("c", CLIENT_NAME) + .param("u", AIRSONIC_USER) + .param("p", "incorrect password") + .param("f", EXPECTED_FORMAT) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.subsonic-response.status").value("failed")) + .andExpect(header().exists(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + public void corsShouldNotBeEnabledForOtherPaths() throws Exception { + mvc.perform(get("/login") + .header("Access-Control-Request-Method", "GET") + .header("Origin", "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().doesNotExist(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + public void testOptionRequest() throws Exception { + mvc.perform(options("/rest/ping") + .header("Access-Control-Request-Method", "GET") + .header("Origin", "https://example.com") + .param("v", AIRSONIC_API_VERSION) + .param("c", CLIENT_NAME) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(header().exists(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + +}