From 5f687f91ce0968e6eef9062b5204baa607f8f0c7 Mon Sep 17 00:00:00 2001 From: Stelios Voutsinas Date: Fri, 21 Jun 2024 22:29:16 -0700 Subject: [PATCH] Added results servlet / Added Capabilities fix / Remove Authenticator --- CHANGELOG.md | 9 + build.gradle | 4 +- .../opencadc/tap/impl/AuthenticatorImpl.java | 152 ------------ .../org/opencadc/tap/impl/CapGetAction.java | 232 ++++++++++++++++++ .../org/opencadc/tap/impl/CapInitAction.java | 208 ++++++++++++++++ .../opencadc/tap/impl/ResultStoreImpl.java | 9 +- .../org/opencadc/tap/impl/ResultsServlet.java | 50 ++++ src/main/webapp/WEB-INF/web.xml | 27 +- .../opencadc/tap/impl/ResultsServletTest.java | 66 +++++ 9 files changed, 598 insertions(+), 159 deletions(-) delete mode 100644 src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java create mode 100644 src/main/java/org/opencadc/tap/impl/CapGetAction.java create mode 100644 src/main/java/org/opencadc/tap/impl/CapInitAction.java create mode 100644 src/main/java/org/opencadc/tap/impl/ResultsServlet.java create mode 100644 src/test/java/org/opencadc/tap/impl/ResultsServletTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 58de45e..d36a7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ lsst-tap-service is versioned with [semver](https://semver.org/). Dependencies a Find changes for the upcoming release in the project's [changelog.d](https://github.com/lsst-sqre/lsst-tap-service/tree/main/changelog.d/). +# 2024-06-28 + +### Fixed + +- Fixed Capabilities handling. Use new CapGetAction & CapInitAction, modified by getting pathPrefix from ENV property + +## Other Changes + +- Change result handling, to use a redirect servlet. Addresses issue with async failing due to auth header propagation with clients like pyvo, topcat # 2024-06-18 diff --git a/build.gradle b/build.gradle index 6933941..bd76fb9 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ dependencies { implementation 'org.opencadc:cadc-jsqlparser-compat:0.6.5' implementation 'org.opencadc:cadc-log:1.2.1' implementation 'org.opencadc:cadc-registry:1.7.6' - implementation 'org.opencadc:cadc-rest:1.3.18' + implementation 'org.opencadc:cadc-rest:1.3.20' implementation 'org.opencadc:cadc-tap:1.1.16' implementation 'org.opencadc:cadc-tap-schema:1.1.32' implementation 'org.opencadc:cadc-tap-server:1.1.23' @@ -81,7 +81,7 @@ dependencies { implementation 'software.amazon.awssdk:auth:2.17.230' implementation 'org.apache.solr:solr-s3-repository:8.11.2' - runtimeOnly 'org.opencadc:cadc-registry:[1.2.1,)' + // runtimeOnly 'org.opencadc:cadc-registry:[1.2.1,)' // https://mvnrepository.com/artifact/com.google.code.gson/gson implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6' diff --git a/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java b/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java deleted file mode 100644 index 22374fa..0000000 --- a/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java +++ /dev/null @@ -1,152 +0,0 @@ -package org.opencadc.tap.impl; - -import java.io.IOException; -import java.net.http.HttpClient; -import java.net.http.HttpResponse; -import java.net.http.HttpResponse.BodyHandlers; -import java.net.http.HttpRequest; -import java.net.URI; -import java.security.AccessControlException; -import java.security.Principal; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import javax.security.auth.Subject; -import javax.security.auth.x500.X500Principal; - -import ca.nrc.cadc.auth.Authenticator; -import ca.nrc.cadc.auth.AuthMethod; -import ca.nrc.cadc.auth.AuthorizationTokenPrincipal; -import ca.nrc.cadc.auth.HttpPrincipal; -import ca.nrc.cadc.auth.NumericPrincipal; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; - -import org.apache.log4j.Logger; - -/** - * @deprecated This class is deprecated and will be removed in future releases. - * The TAP Service now uses IdentityManager for authentication, available in the opencadc library - * - * @author cbanek - */ -@Deprecated -public class AuthenticatorImpl implements Authenticator -{ - private static final Logger log = Logger.getLogger(AuthenticatorImpl.class); - - // Size of the token cache is read from the maxTokenCache property, with - // a default of 1000 tokens cached. - private static final int maxTokenCache = Integer.getInteger("maxTokenCache", 1000); - - private static final String gafaelfawr_url = System.getProperty("gafaelfawr_url"); - - private static final HttpClient client = HttpClient.newHttpClient(); - - private static final ConcurrentHashMap tokenCache = new ConcurrentHashMap<>(); - - private final class TokenInfo - { - public final String username; - public final int uid; - - public TokenInfo(String username, int uid) - { - this.username = username; - this.uid = uid; - } - } - - public AuthenticatorImpl() - { - } - - public Subject validate(Subject subject) throws AccessControlException { - log.debug("Subject to augment starts as: " + subject); - - // Check if the cache is too big, and if so, clear it out. - if (tokenCache.size() > maxTokenCache) { - tokenCache.clear(); - } - - List addedPrincipals = new ArrayList(); - AuthorizationTokenPrincipal tokenPrincipal = null; - - for (Principal principal : subject.getPrincipals()) { - if (principal instanceof AuthorizationTokenPrincipal) { - tokenPrincipal = (AuthorizationTokenPrincipal) principal; - TokenInfo tokenInfo = null; - - for (int i = 1; i < 5 && tokenInfo == null; i++) { - try { - tokenInfo = getTokenInfo(tokenPrincipal.getHeaderValue()); - } catch (IOException|InterruptedException e) { - log.warn("Exception thrown while getting info from Gafaelfawr"); - log.warn(e); - } - } - - if (tokenInfo != null) { - X500Principal xp = new X500Principal("CN=" + tokenInfo.username); - addedPrincipals.add(xp); - - HttpPrincipal hp = new HttpPrincipal(tokenInfo.username); - addedPrincipals.add(hp); - - UUID uuid = new UUID(0L, (long) tokenInfo.uid); - NumericPrincipal np = new NumericPrincipal(uuid); - addedPrincipals.add(np); - } - else { - log.error("Gave up retrying user-info requests to Gafaelfawr"); - } - } - } - - if (tokenPrincipal != null) { - subject.getPrincipals().remove(tokenPrincipal); - } - - subject.getPrincipals().addAll(addedPrincipals); - subject.getPublicCredentials().add(AuthMethod.TOKEN); - - log.debug("Augmented subject is " + subject); - return subject; - } - - // Here we could check the token again, but gafaelfawr should be - // doing that for us already by the time it gets to us. So for - // this layer, we just let this go through. - public Subject augment(Subject subject) { - return subject; - } - - private TokenInfo getTokenInfo(String token) throws IOException, InterruptedException { - // If the request has gotten this far, the token has already - // been checked upstream, so we know it's valid, we just need - // to determine the uid and the username. - if (!tokenCache.containsKey(token)) { - HttpRequest request = HttpRequest.newBuilder(URI.create(gafaelfawr_url)) - .header("Accept", "application/json") - .header("Authorization", token) - .build(); - - HttpResponse response = client.send(request, BodyHandlers.ofString()); - String body = response.body(); - - Gson gson = new Gson(); - JsonObject authData = gson.fromJson(body, JsonObject.class); - String username = authData.getAsJsonPrimitive("username").getAsString(); - int uid = authData.getAsJsonPrimitive("uid").getAsInt(); - - // Insert the info into the cache here since we retrieved it. - tokenCache.put(token, new TokenInfo(username, uid)); - } - - return tokenCache.get(token); - } -} diff --git a/src/main/java/org/opencadc/tap/impl/CapGetAction.java b/src/main/java/org/opencadc/tap/impl/CapGetAction.java new file mode 100644 index 0000000..af9f40b --- /dev/null +++ b/src/main/java/org/opencadc/tap/impl/CapGetAction.java @@ -0,0 +1,232 @@ +/* +************************************************************************ +******************* CANADIAN ASTRONOMY DATA CENTRE ******************* +************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** +* +* (c) 2020. (c) 2020. +* Government of Canada Gouvernement du Canada +* National Research Council Conseil national de recherches +* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +* All rights reserved Tous droits réservés +* +* NRC disclaims any warranties, Le CNRC dénie toute garantie +* expressed, implied, or énoncée, implicite ou légale, +* statutory, of any kind with de quelque nature que ce +* respect to the software, soit, concernant le logiciel, +* including without limitation y compris sans restriction +* any warranty of merchantability toute garantie de valeur +* or fitness for a particular marchande ou de pertinence +* purpose. NRC shall not be pour un usage particulier. +* liable in any event for any Le CNRC ne pourra en aucun cas +* damages, whether direct or être tenu responsable de tout +* indirect, special or general, dommage, direct ou indirect, +* consequential or incidental, particulier ou général, +* arising from the use of the accessoire ou fortuit, résultant +* software. Neither the name de l'utilisation du logiciel. Ni +* of the National Research le nom du Conseil National de +* Council of Canada nor the Recherches du Canada ni les noms +* names of its contributors may de ses participants ne peuvent +* be used to endorse or promote être utilisés pour approuver ou +* products derived from this promouvoir les produits dérivés +* software without specific prior de ce logiciel sans autorisation +* written permission. préalable et particulière +* par écrit. +* +* This file is part of the Ce fichier fait partie du projet +* OpenCADC project. OpenCADC. +* +* OpenCADC is free software: OpenCADC est un logiciel libre ; +* you can redistribute it and/or vous pouvez le redistribuer ou le +* modify it under the terms of modifier suivant les termes de +* the GNU Affero General Public la “GNU Affero General Public +* License as published by the License” telle que publiée +* Free Software Foundation, par la Free Software Foundation +* either version 3 of the : soit la version 3 de cette +* License, or (at your option) licence, soit (à votre gré) +* any later version. toute version ultérieure. +* +* OpenCADC is distributed in the OpenCADC est distribué +* hope that it will be useful, dans l’espoir qu’il vous +* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +* without even the implied GARANTIE : sans même la garantie +* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF +* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +* General Public License for Générale Publique GNU Affero +* more details. pour plus de détails. +* +* You should have received Vous devriez avoir reçu une +* a copy of the GNU Affero copie de la Licence Générale +* General Public License along Publique GNU Affero avec +* with OpenCADC. If not, see OpenCADC ; si ce n’est +* . pas le cas, consultez : +* . +* +************************************************************************ +*/ + +package org.opencadc.tap.impl; + +import ca.nrc.cadc.auth.AuthMethod; +import ca.nrc.cadc.auth.AuthenticationUtil; +import ca.nrc.cadc.auth.NotAuthenticatedException; +import ca.nrc.cadc.net.HttpTransfer; +import ca.nrc.cadc.net.ResourceNotFoundException; +import ca.nrc.cadc.reg.AccessURL; +import ca.nrc.cadc.reg.Capabilities; +import ca.nrc.cadc.reg.CapabilitiesWriter; +import ca.nrc.cadc.reg.Capability; +import ca.nrc.cadc.reg.Interface; +import ca.nrc.cadc.reg.Standards; +import ca.nrc.cadc.reg.client.LocalAuthority; +import ca.nrc.cadc.reg.client.RegistryClient; +import ca.nrc.cadc.rest.InlineContentHandler; +import ca.nrc.cadc.rest.RestAction; +import ca.nrc.cadc.rest.SyncOutput; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Set; +import java.util.TreeSet; +import org.opencadc.tap.impl.CapInitAction; +import org.apache.log4j.Logger; + +/** + * + * @author pdowler + */ +public class CapGetAction extends RestAction { + private static final Logger log = Logger.getLogger(CapGetAction.class); + private static final String baseURL = System.getProperty("base_url"); + private static final String pathPrefix = System.getProperty("path_prefix"); + + /** + * Enable transformation of the capabilities template (default: true). Subclasses + * may disable this according to some policy. The current transform is to change + * the host name in every accessURL in the capabilities to match the host name used + * in the request. This works fine in most cases but would not work + * if some accessURL(s) within an application are deployed on a different host. + * For example, if the VOSI-availability endpoint is deployed on an separate host + * so it can probe the service from the outside, then capabilities transform + * would need to be disabled. + */ + protected boolean doTransform = true; + + public CapGetAction() { + super(); + } + + @Override + protected String getServerImpl() { + return CapInitAction.getVersion(componentID); + } + + @Override + protected InlineContentHandler getInlineContentHandler() { + return null; + } + + @Override + public void doAction() throws Exception { + if (CapInitAction.getAuthRequired(componentID)) { + AuthMethod am = AuthenticationUtil.getAuthMethod(AuthenticationUtil.getCurrentSubject()); + if (am == null || am.equals(AuthMethod.ANON)) { + throw new NotAuthenticatedException("permission denied"); + } + } + + Capabilities caps = CapInitAction.getTemplate(componentID); + + log.debug("transformAccessURL=" + doTransform); + + if (doTransform) { + transform(caps); + } + + doOutput(caps, syncOutput); + logInfo.setSuccess(true); + } + + // transform all accessURL so the hostname and context path match that used to invoke + // the /capabilities endpoint + private void transform(Capabilities caps) throws MalformedURLException { + log.debug("context: " + syncInput.getContextPath()); + log.debug("component: " + syncInput.getComponentPath()); + + String hostname = new URL(syncInput.getRequestURI()).getHost(); + + // find context path in the template using capabilities endpoint + Capability cap = caps.findCapability(Standards.VOSI_CAPABILITIES); + URL capURL = cap.getInterfaces().get(0).getAccessURL().getURL(); + String capPath = capURL.getPath(); + String basePath = capURL.getPath().substring(0, capPath.indexOf("/capabilities")); // chop + + // capabilities in the request + String actualPath = syncInput.getContextPath() + syncInput.getComponentPath(); + actualPath = actualPath.substring(0, actualPath.indexOf("/capabilities")); // chop + + log.debug("transform: basePath in template: " + basePath + " actualPath: " + actualPath); + for (Capability c : caps.getCapabilities()) { + for (Interface i : c.getInterfaces()) { + AccessURL u = i.getAccessURL(); + URL url = u.getURL(); + String path = url.getPath(); + String npath = path.replace(basePath, pathPrefix); + URL nurl = new URL(url.getProtocol(), hostname, npath); + u.setURL(nurl); + log.debug("transform: " + url + " -> " + nurl); + } + } + } + + private void doOutput(Capabilities caps, SyncOutput out) throws IOException { + out.setHeader(HttpTransfer.CONTENT_TYPE, "text/xml"); + out.setCode(200); + CapabilitiesWriter w = new CapabilitiesWriter(); + w.write(caps, syncOutput.getOutputStream()); + } + + + private void injectAuthProviders(Capabilities caps) throws IOException { + Set sms = new TreeSet<>(); + for (Capability cap : caps.getCapabilities()) { + for (Interface i : cap.getInterfaces()) { + for (URI s : i.getSecurityMethods()) { + sms.add(s); + } + } + + } + log.debug("found " + sms.size() + " unique SecurityMethod(s)"); + if (sms.isEmpty()) { + return; + } + + LocalAuthority loc = new LocalAuthority(); + RegistryClient reg = new RegistryClient(); + for (URI sm : sms) { + URI resourceID = loc.getServiceURI(sm.toASCIIString()); + try { + if (resourceID != null) { + Capabilities srv = reg.getCapabilities(resourceID); + if (srv != null) { + Capability auth = srv.findCapability(sm); + if (auth != null) { + caps.getCapabilities().add(auth); + } else { + log.debug("not found: " + sm + " in " + resourceID); + } + } else { + log.debug("not found: " + resourceID + " capabilities"); + } + } else { + log.debug("not found: " + sm); + } + } catch (ResourceNotFoundException ex) { + log.warn("failed to find auth service: " + resourceID + "cause: " + ex); + } + } + } + +} diff --git a/src/main/java/org/opencadc/tap/impl/CapInitAction.java b/src/main/java/org/opencadc/tap/impl/CapInitAction.java new file mode 100644 index 0000000..c99ce4a --- /dev/null +++ b/src/main/java/org/opencadc/tap/impl/CapInitAction.java @@ -0,0 +1,208 @@ +/* +************************************************************************ +******************* CANADIAN ASTRONOMY DATA CENTRE ******************* +************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** +* +* (c) 2024. (c) 2024. +* Government of Canada Gouvernement du Canada +* National Research Council Conseil national de recherches +* Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +* All rights reserved Tous droits réservés +* +* NRC disclaims any warranties, Le CNRC dénie toute garantie +* expressed, implied, or énoncée, implicite ou légale, +* statutory, of any kind with de quelque nature que ce +* respect to the software, soit, concernant le logiciel, +* including without limitation y compris sans restriction +* any warranty of merchantability toute garantie de valeur +* or fitness for a particular marchande ou de pertinence +* purpose. NRC shall not be pour un usage particulier. +* liable in any event for any Le CNRC ne pourra en aucun cas +* damages, whether direct or être tenu responsable de tout +* indirect, special or general, dommage, direct ou indirect, +* consequential or incidental, particulier ou général, +* arising from the use of the accessoire ou fortuit, résultant +* software. Neither the name de l'utilisation du logiciel. Ni +* of the National Research le nom du Conseil National de +* Council of Canada nor the Recherches du Canada ni les noms +* names of its contributors may de ses participants ne peuvent +* be used to endorse or promote être utilisés pour approuver ou +* products derived from this promouvoir les produits dérivés +* software without specific prior de ce logiciel sans autorisation +* written permission. préalable et particulière +* par écrit. +* +* This file is part of the Ce fichier fait partie du projet +* OpenCADC project. OpenCADC. +* +* OpenCADC is free software: OpenCADC est un logiciel libre ; +* you can redistribute it and/or vous pouvez le redistribuer ou le +* modify it under the terms of modifier suivant les termes de +* the GNU Affero General Public la “GNU Affero General Public +* License as published by the License” telle que publiée +* Free Software Foundation, par la Free Software Foundation +* either version 3 of the : soit la version 3 de cette +* License, or (at your option) licence, soit (à votre gré) +* any later version. toute version ultérieure. +* +* OpenCADC is distributed in the OpenCADC est distribué +* hope that it will be useful, dans l’espoir qu’il vous +* but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +* without even the implied GARANTIE : sans même la garantie +* warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +* or FITNESS FOR A PARTICULAR ni d’ADÉQUATION À UN OBJECTIF +* PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +* General Public License for Générale Publique GNU Affero +* more details. pour plus de détails. +* +* You should have received Vous devriez avoir reçu une +* a copy of the GNU Affero copie de la Licence Générale +* General Public License along Publique GNU Affero avec +* with OpenCADC. If not, see OpenCADC ; si ce n’est +* . pas le cas, consultez : +* . +* +************************************************************************ +*/ + +package org.opencadc.tap.impl; + +import ca.nrc.cadc.reg.Capabilities; +import ca.nrc.cadc.reg.CapabilitiesReader; +import ca.nrc.cadc.rest.InitAction; +import ca.nrc.cadc.rest.Version; +import ca.nrc.cadc.util.StringUtil; + +import java.io.StringReader; +import java.net.URL; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +import org.apache.log4j.Logger; + +/** + * InitAction implementation for VOSI-capabilities from template xml file. + * + * @author pdowler + */ +public class CapInitAction extends InitAction { + private static final Logger log = Logger.getLogger(CapInitAction.class); + + public CapInitAction() { + super(); + } + + static Capabilities getTemplate(String componentID) { + String jndiKey = componentID + ".cap-template"; + try { + log.debug("retrieving capabilities template via JNDI: " + jndiKey); + Context initContext = new InitialContext(); + String tmpl = (String) initContext.lookup(jndiKey); + CapabilitiesReader cr = new CapabilitiesReader(false); // validated in doInit + StringReader sr = new StringReader(tmpl); + Capabilities caps = cr.read(sr); + return caps; + } catch (Exception ex) { + throw new IllegalStateException("failed to find template via JNDI: init failed", ex); + } + } + + static boolean getAuthRequired(String componentID) { + String jndiKey = componentID + ".authRequired"; + try { + log.debug("retrieving authRequired via JNDI: " + jndiKey); + Context initContext = new InitialContext(); + Boolean authRequired = (Boolean) initContext.lookup(jndiKey); + if (authRequired == null) { + return false; + } + return authRequired; + } catch (Exception ex) { + throw new IllegalStateException("failed to find authRequired via JNDI: init failed", ex); + } + } + + static String getVersion(String componentID) { + String jndiKey = componentID + ".version"; + try { + log.debug("retrieving version via JNDI: " + jndiKey); + Context initContext = new InitialContext(); + String version = (String) initContext.lookup(jndiKey); + return version; + } catch (Exception ex) { + throw new IllegalStateException("failed to find version via JNDI: init failed", ex); + } + } + + @Override + public void doInit() { + + final Context initContext; + try { + initContext = new InitialContext(); + } catch (NamingException ex) { + throw new IllegalStateException("failed to find JDNI InitialContext", ex); + } + + String jndiKey = componentID + ".cap-template"; + String str = initParams.get("input"); + + log.debug("doInit: static capabilities: " + str); + try { + URL resURL = super.getResource(str); + String tmpl = StringUtil.readFromInputStream(resURL.openStream(), "UTF-8"); + + // validate + CapabilitiesReader cr = new CapabilitiesReader(); + cr.read(tmpl); + try { + log.debug("unbinding possible existing template"); + initContext.unbind(jndiKey); + } catch (NamingException e) { + log.debug("no previously bound template, continuting"); + } + initContext.bind(jndiKey, tmpl); + log.info("doInit: capabilities template=" + str + " stored via JNDI: " + jndiKey); + } catch (Exception ex) { + throw new IllegalArgumentException("CONFIG: failed to read capabilities template: " + str, ex); + } + + try { + String authRequired = initParams.get("authRequired"); + jndiKey = componentID + ".authRequired"; + try { + log.debug("unbinding possible authRequired value"); + initContext.unbind(jndiKey); + } catch (NamingException e) { + log.debug("no previously bound value, continuting"); + } + if ("true".equals(authRequired)) { + initContext.bind(jndiKey, Boolean.TRUE); + log.info("doInit: authRequired=true stored via JNDI: " + jndiKey); + } else { + initContext.bind(jndiKey, Boolean.FALSE); + log.info("doInit: authRequired=false stored via JNDI: " + jndiKey); + } + } catch (Exception ex) { + throw new IllegalArgumentException("CONFIG: failed to set authRequired flag", ex); + } + + try { + Version version = getLibraryVersion(CapInitAction.class); + + jndiKey = componentID + ".version"; + try { + log.debug("unbinding possible version value"); + initContext.unbind(jndiKey); + } catch (NamingException e) { + log.debug("no previously bound value, continuting"); + } + initContext.bind(jndiKey, version.getMajorMinor()); + log.info("doInit: version=" + version + " stored via JNDI: " + jndiKey); + } catch (Exception ex) { + throw new IllegalArgumentException("CONFIG: failed to set version flag", ex); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/opencadc/tap/impl/ResultStoreImpl.java b/src/main/java/org/opencadc/tap/impl/ResultStoreImpl.java index 2a432b2..9febc78 100644 --- a/src/main/java/org/opencadc/tap/impl/ResultStoreImpl.java +++ b/src/main/java/org/opencadc/tap/impl/ResultStoreImpl.java @@ -97,7 +97,8 @@ public class ResultStoreImpl implements ResultStore { private static final String bucket = System.getProperty("gcs_bucket"); private static final String bucketURL = System.getProperty("gcs_bucket_url"); private static final String bucketType = System.getProperty("gcs_bucket_type"); - + private static final String baseURL = System.getProperty("base_url"); + private static final String pathPrefix = System.getProperty("path_prefix"); @Override public URL put(final ResultSet resultSet, @@ -166,11 +167,13 @@ private OutputStream getOutputStreamGCS() { } private URL getURL() throws MalformedURLException { + String filepath = ""; if (bucketType.equals(new String("S3"))) { - return new URL(new URL(bucketURL), "/"+bucket+"/"+filename); + filepath = "/" + bucket + "/" + filename; } else { - return new URL(new URL(bucketURL), filename); + filepath = filename; } + return new URL(baseURL + pathPrefix + "/results/" + filepath); } private URI getURI() { diff --git a/src/main/java/org/opencadc/tap/impl/ResultsServlet.java b/src/main/java/org/opencadc/tap/impl/ResultsServlet.java new file mode 100644 index 0000000..4191445 --- /dev/null +++ b/src/main/java/org/opencadc/tap/impl/ResultsServlet.java @@ -0,0 +1,50 @@ +package org.opencadc.tap.impl; + +import org.apache.log4j.Logger; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * A servlet that handles redirecting to specific job results. + * This servlet extracts the VOTable file name from the request path and constructs a URL to redirect the client. + * + * @author stvoutsin + */ +public class ResultsServlet extends HttpServlet { + private static final Logger log = Logger.getLogger(ResultsServlet.class); + private static final String bucketURL = System.getProperty("gcs_bucket_url"); + + /** + * Processes GET requests by extracting the result filename from the request path and redirecting to the corresponding results URL. + * The filename is assumed to be the path info of the request URL, following the first '/' character. + * + * @param request the HttpServletRequest object that contains the request + * @param response the HttpServletResponse object that contains the response + * @throws ServletException if an input or output error is detected when the servlet handles the GET request + * @throws IOException if the request for the GET could not be handled + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + try { + String path = request.getPathInfo(); + String redirectUrl = generateRedirectUrl(bucketURL, path); + response.sendRedirect(redirectUrl); + } catch (Exception e) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "An error occurred while processing the request."); + } + } + + /** + * Generates the redirect URL based on a path. + * + * @param path the request path + * @return the redirect URL constructed using the bucket URL and results file + */ + private String generateRedirectUrl(String bucketUrlString, String path) { + String resultsFile = path.substring(1); + return bucketUrlString + "/" + resultsFile; + } +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index d4cf863..d5a4beb 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -76,10 +76,23 @@ org.opencadc.tap.ws.QueryJobManager - + + CapabilitiesServlet - ca.nrc.cadc.vosi.CapabilitiesServlet + ca.nrc.cadc.rest.RestServlet + + init + org.opencadc.tap.impl.CapInitAction + + + head + ca.nrc.cadc.vosi.CapHeadAction + + + get + org.opencadc.tap.impl.CapGetAction + input /capabilities.xml @@ -106,6 +119,16 @@ 3 + + ResultsServlet + org.opencadc.tap.impl.ResultsServlet + + + + ResultsServlet + /results/* + + AsyncServlet diff --git a/src/test/java/org/opencadc/tap/impl/ResultsServletTest.java b/src/test/java/org/opencadc/tap/impl/ResultsServletTest.java new file mode 100644 index 0000000..015216e --- /dev/null +++ b/src/test/java/org/opencadc/tap/impl/ResultsServletTest.java @@ -0,0 +1,66 @@ +package org.opencadc.tap.impl; + +import ca.nrc.cadc.tap.schema.ColumnDesc; +import ca.nrc.cadc.tap.schema.SchemaDesc; +import ca.nrc.cadc.tap.schema.TableDesc; +import ca.nrc.cadc.tap.schema.TapDataType; +import ca.nrc.cadc.tap.schema.TapSchema; +import ca.nrc.cadc.util.Log4jInit; +import ca.nrc.cadc.uws.Job; +import ca.nrc.cadc.uws.Parameter; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.junit.Assert; +import org.junit.Test; +import static org.junit.Assert.assertEquals; +import java.lang.reflect.Method; + +public class ResultsServletTest { + + private static final Logger log = Logger.getLogger(ResultsServletTest.class); + + static + { + Log4jInit.setLevel("org.opencadc.tap.impl", Level.INFO); + + } + + Job job = new Job() + { + @Override + public String getID() { return "testJob"; } + }; + + public ResultsServletTest() { } + + @Test + public void testGenerateRedirectUrl() throws Exception { + String bucketUrl = "https://tap-files.lsst.codes"; + String expectedUrl = "https://tap-files.lsst.codes/result_qz4z5hf6qy5509p1.xml"; + ResultsServlet resultsServlet = new ResultsServlet(); + resultsServlet.init(); + String path = "/result_qz4z5hf6qy5509p1.xml"; + + Method method = ResultsServlet.class.getDeclaredMethod("generateRedirectUrl", String.class, String.class); + method.setAccessible(true); + String actualUrl = (String) method.invoke(resultsServlet, bucketUrl, path); + + assertEquals(expectedUrl, actualUrl); + } + + @Test + public void testGenerateRedirectUrlWithBucket() throws Exception { + String bucketUrl = "https://tap-files.lsst.codes"; + String expectedUrl = "https://tap-files.lsst.codes/bucket12345/result_qz4z5hf6qy5509p1.xml"; + ResultsServlet resultsServlet = new ResultsServlet(); + resultsServlet.init(); + String path = "/bucket12345/result_qz4z5hf6qy5509p1.xml"; + + Method method = ResultsServlet.class.getDeclaredMethod("generateRedirectUrl", String.class, String.class); + method.setAccessible(true); + String actualUrl = (String) method.invoke(resultsServlet, bucketUrl, path); + + assertEquals(expectedUrl, actualUrl); + } + +} \ No newline at end of file