diff --git a/src/main/java/it/inaf/oats/vospace/CapabilitiesController.java b/src/main/java/it/inaf/oats/vospace/CapabilitiesController.java new file mode 100644 index 0000000000000000000000000000000000000000..b767264f3293463022a98c953709d01cc79f1bc8 --- /dev/null +++ b/src/main/java/it/inaf/oats/vospace/CapabilitiesController.java @@ -0,0 +1,56 @@ +package it.inaf.oats.vospace; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; +import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +@RestController +public class CapabilitiesController { + + @Autowired + private HttpServletRequest request; + + @GetMapping(value = "/capabilities", produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE}) + public String getCapabilities() throws IOException { + String xml = loadCapabilitiesXmlTemplate(); + return xml.replace("{{ base_url }}", getBaseUrl()); + } + + private String loadCapabilitiesXmlTemplate() throws IOException { + try ( InputStream in = CapabilitiesController.class.getClassLoader().getResourceAsStream("capabilities.xml")) { + Scanner s = new Scanner(in).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + } + + /** + * Generate base URL considering also proxied requests. + */ + private String getBaseUrl() { + + String forwaredProtocol = request.getHeader("X-Forwarded-Proto"); + String scheme = forwaredProtocol != null ? forwaredProtocol : request.getScheme(); + + String forwardedHost = request.getHeader("X-Forwarded-Host"); + if (forwardedHost != null && forwardedHost.contains(",")) { + // X-Forwarded-Host can be a list of comma separated values + forwardedHost = forwardedHost.split(",")[0]; + } + String host = forwardedHost != null ? forwardedHost : request.getServerName(); + + UriComponentsBuilder builder = UriComponentsBuilder.newInstance() + .scheme(scheme).host(host).path(request.getContextPath()); + + if (forwardedHost == null) { + builder.port(request.getServerPort()); + } + + return builder.toUriString(); + } +} diff --git a/src/main/resources/capabilities.xml b/src/main/resources/capabilities.xml new file mode 100644 index 0000000000000000000000000000000000000000..45f66147063e8e2f4380b04b37896c8f232e7cfc --- /dev/null +++ b/src/main/resources/capabilities.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<vosi:capabilities xmlns:vosi="http://www.ivoa.net/xml/VOSICapabilities/v1.0" xmlns:vs="http://www.ivoa.net/xml/VODataService/v1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <capability standardID="ivo://ivoa.net/std/VOSI#capabilities"> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/capabilities</accessURL> + </interface> + </capability> + <capability standardID="ivo://ivoa.net/std/VOSI#availability"> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/availability</accessURL> + </interface> + </capability> + <capability standardID="ivo://ivoa.net/std/VOSpace/v2.0#views"> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/views</accessURL> + </interface> + </capability> + <capability standardID="ivo://ivoa.net/std/VOSpace/v2.0#nodes"> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="base">{{ base_url }}/nodes</accessURL> + </interface> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="base">{{ base_url }}/nodes</accessURL> + <securityMethod standardID="vos://cadc.nrc.ca~vospace/CADC/std/Auth#token-1.0"/> + </interface> + </capability> + <capability standardID="ivo://ivoa.net/std/VOSpace/v2.0#transfers"> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/transfers</accessURL> + </interface> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/transfers</accessURL> + <securityMethod standardID="vos://cadc.nrc.ca~vospace/CADC/std/Auth#token-1.0"/> + </interface> + </capability> + <capability standardID="ivo://ivoa.net/std/VOSpace/v2.0#sync"> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/synctrans</accessURL> + </interface> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/synctrans</accessURL> + <securityMethod standardID="vos://cadc.nrc.ca~vospace/CADC/std/Auth#token-1.0"/> + </interface> + </capability> + <capability standardID="ivo://ivoa.net/std/VOSpace#sync-2.1"> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/synctrans</accessURL> + </interface> + <interface xsi:type="vs:ParamHTTP" role="std"> + <accessURL use="full">{{ base_url }}/synctrans</accessURL> + <securityMethod standardID="vos://cadc.nrc.ca~vospace/CADC/std/Auth#token-1.0"/> + </interface> + </capability> +</vosi:capabilities> \ No newline at end of file diff --git a/src/test/java/it/inaf/oats/vospace/CapabilitiesControllerTest.java b/src/test/java/it/inaf/oats/vospace/CapabilitiesControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ae3a1812768bd5574e0805c6a12843880624a818 --- /dev/null +++ b/src/test/java/it/inaf/oats/vospace/CapabilitiesControllerTest.java @@ -0,0 +1,62 @@ +package it.inaf.oats.vospace; + +import javax.servlet.http.HttpServletRequest; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ExtendWith(MockitoExtension.class) +public class CapabilitiesControllerTest { + + private MockMvc mockMvc; + + @Mock + private HttpServletRequest request; + + @InjectMocks + private CapabilitiesController controller; + + @BeforeEach + public void init() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + public void testGetCapabilitiesBase() throws Exception { + + when(request.getServerName()).thenReturn("ia2.inaf.it"); + when(request.getServerPort()).thenReturn(8080); + when(request.getContextPath()).thenReturn("/vospace"); + when(request.getScheme()).thenReturn("http"); + + String xml = mockMvc.perform(get("/capabilities")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertTrue(xml.contains("http://ia2.inaf.it:8080/vospace/nodes")); + } + + @Test + public void testGetCapabilitiesProxied() throws Exception { + + when(request.getHeader("X-Forwarded-Proto")).thenReturn("https"); + when(request.getHeader("X-Forwarded-Host")).thenReturn("ia2.inaf.it,server2.ia2.inaf.it"); + + when(request.getContextPath()).thenReturn("/vospace"); + + String xml = mockMvc.perform(get("/capabilities")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + assertTrue(xml.contains("https://ia2.inaf.it/vospace/nodes")); + } +}