Commit 0ec09b65 authored by Dustin Jenkins's avatar Dustin Jenkins
Browse files

Story 1874: Add Content-Disposition to download file.

parent be5717fd
Loading
Loading
Loading
Loading
+41 −42
Original line number Diff line number Diff line
@@ -67,9 +67,8 @@
************************************************************************
-->


<!DOCTYPE project>
<project default="build" basedir=".">
<project name="cadcCDP-Server" default="build" basedir=".">
  <property environment="env"/>
  <property file="local.build.properties"/>

@@ -97,7 +96,8 @@
  <property name="bouncy" value="${ext.lib}/bcprov.jar"/>
  <property name="spring" value="${ext.lib}/spring.jar"/>

    <property name="jars" value="${log4j}:${servlet}:${bouncy}:${cadcLog}:${cadcUtil}:${cadcCDP}:${cadcVOSI}:${spring}" />
  <property name="jars"
            value="${log4j}:${servlet}:${bouncy}:${cadcLog}:${cadcUtil}:${cadcCDP}:${cadcVOSI}:${spring}"/>

  <target name="build" depends="compile">
    <jar jarfile="${build}/lib/${project}.jar"
@@ -108,15 +108,14 @@
  </target>

  <target name="setup-test">
        <copy overwrite="true" file="${env.CADC_PREFIX}/etc/proxy.pem" tofile="${build}/test/class/proxy.pem" />
    <copy overwrite="true" file="${env.CADC_PREFIX}/etc/proxy.pem"
          tofile="${build}/test/class/proxy.pem"/>
  </target>

  <property name="easyMock" value="${ext.dev}/easymock.jar"/>
  <property name="junit" value="${ext.dev}/junit.jar"/>
    <property name="cglib"          value="${ext.dev}/cglib.jar" />
  <property name="objenesis" value="${ext.dev}/objenesis.jar"/>
    <property name="asm"            value="${ext.dev}/asm.jar" />

    <property name="testingJars"    value="${easyMock}:${junit}:${cglib}:${asm}:${objenesis}" />
  <property name="testingJars" value="${junit}:${objenesis}:${easyMock}"/>

</project>
+150 −91
Original line number Diff line number Diff line
@@ -71,9 +71,7 @@ package ca.nrc.cadc.cred.server;

import ca.nrc.cadc.auth.AuthMethod;
import java.io.BufferedWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.AccessControlException;
import java.security.PrivilegedActionException;
import java.util.Collections;
@@ -114,18 +112,23 @@ public class ProxyServlet extends HttpServlet
    public static final String CATALOG = "catalog";
    public static final String SCHEMA = "schema";

    static final String CERTIFICATE_CONTENT_TYPE =
            "application/x-x509-user-cert";
    static final String CERTIFICATE_FILENAME = "cadcproxy.pem";
    
    private static final long serialVersionUID = 2740612605831266225L;
    private static Logger LOGGER = Logger.getLogger(ProxyServlet.class);

    // The set of trusted principals allowed to call this service
    private Map<X500Principal, Float> trustedPrincipals = new HashMap<X500Principal, Float>();
    private Map<X500Principal, Float> trustedPrincipals =
            new HashMap<X500Principal, Float>();
    private String dataSourceName;
    private String database;
    private String schema;

    /**
     * Read the configuration.
     * @param config
     * @param config            The ServletConfig as provided by the container.
     * @throws javax.servlet.ServletException
     */
    @Override
@@ -134,18 +137,20 @@ public class ProxyServlet extends HttpServlet
    {
        super.init(config);
        // get the trusted principals from config
        String trustedPrincipalsValue = config.getInitParameter(TRUSTED_PRINCIPALS_PARAM);
        String trustedPrincipalsValue =
                config.getInitParameter(TRUSTED_PRINCIPALS_PARAM);
        if (trustedPrincipalsValue != null)
        {
            StringTokenizer st = new StringTokenizer(trustedPrincipalsValue, "\n\t\r", false);
            StringTokenizer st = new StringTokenizer(trustedPrincipalsValue,
                                                     "\n\t\r", false);
            while (st.hasMoreTokens())
            {
                String principalStr = st.nextToken();
                StringTokenizer st2 = new StringTokenizer(principalStr, ":", false);
                String principal = null; // the principal of the
                // trusted client
                Float maxDaysValid = null; // maximum lifetime of the
                // returned proxy
                StringTokenizer st2 = new StringTokenizer(principalStr, ":",
                                                          false);
                final String principal; // the principal of the trusted client
                final Float maxDaysValid; // maximum lifetime of the returned proxy

                if (st2.countTokens() == 1)
                {
                    principal = principalStr.trim();
@@ -158,17 +163,20 @@ public class ProxyServlet extends HttpServlet
                    if (maxDaysValid <= 0)
                    {
                        throw new IllegalArgumentException(
                                "Maximum valid days must be positive, " + maxDaysValid);
                                "Maximum valid days must be positive, "
                                + maxDaysValid);
                    }
                }
                else
                {
                    throw new IllegalArgumentException(
                            "Cannot parse trusted principal from servlet config: "
                                    + principalStr);
                            "Cannot parse trusted principal from servlet " +
                            "config: " + principalStr);
                }
                LOGGER.info("trusted: " + principal + " , max days valid: " + maxDaysValid);
                trustedPrincipals.put(new X500Principal(principal), maxDaysValid);
                LOGGER.info("trusted: " + principal + " , max days valid: "
                            + maxDaysValid);
                trustedPrincipals.put(new X500Principal(principal),
                                      maxDaysValid);
            }
        }
        
@@ -176,67 +184,91 @@ public class ProxyServlet extends HttpServlet
        this.database = config.getInitParameter(CATALOG);
        this.schema = config.getInitParameter(SCHEMA);
        
        LOGGER.info("persistence: " + dataSourceName + " " + database + " " + schema);
        LOGGER.info("persistence: " + dataSourceName + " " + database + " "
                    + schema);
    }

    /**
     * Handles the HTTP <code>GET</code> method.
     * Obtain the current Subject.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws java.io.IOException
     * @param request       The HTTP Request.
     * @return              Subject for the current Request, or null if none.
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
    Subject getCurrentSubject(final HttpServletRequest request)
            throws IOException
    {
        WebServiceLogInfo logInfo = new ServletLogInfo(request);
        LOGGER.info(logInfo.start());
        long start = System.currentTimeMillis();
        try
        {
            Subject subject = AuthenticationUtil.getSubject(request);
            logInfo.setSubject(subject);
        return AuthenticationUtil.getSubject(request);
    }

    /**
     * Obtain the current X509 certificate chain.
     * @param request       The HTTP Request.
     * @param subject       The current Subject.
     * @return              X509CertificateChain instance.
     * @throws Exception
     */
    X509CertificateChain getX509CertificateChain(
            final HttpServletRequest request, final Subject subject)
            throws Exception
    {
        AuthMethod am = AuthenticationUtil.getAuthMethod(subject);
            if (am == null || AuthMethod.ANON.equals(am))
        if ((am == null) || AuthMethod.ANON.equals(am))
        {
            throw new AccessControlException("permission denied");
        }

        DelegationActionFactory factory = new DelegationActionFactory(
                request, trustedPrincipals, dataSourceName, database, schema);
        DelegationAction delegationAction = factory.getDelegationAction();

            X509CertificateChain certkey;
        X509CertificateChain certificateChain;
        try
        {
                certkey = Subject.doAs(subject, delegationAction);
            certificateChain = Subject.doAs(subject, delegationAction);
        }
        catch(PrivilegedActionException ex)
        {
            throw ex.getException();
        }

            if (certkey.getChain() == null)
        if (certificateChain.getChain() == null)
        {
            throw new ResourceNotFoundException("No signed certificate");
        }
        else
        {
            return certificateChain;
        }
    }

            // this is streamed directly, so there is no way to set the content length
    /**
     * Write out the certificate chain to the response as a download.
     *
     * @param certificateChain      The X509CertificateChain instance to write.
     * @param response              The HTTP Response.
     * @param logInfo               The logging object to update.
     * @throws Exception
     */
    void writeCertificateChain(final X509CertificateChain certificateChain,
                               final HttpServletResponse response,
                               final WebServiceLogInfo logInfo)
            throws Exception
    {
        // This is streamed directly, so there is no way to set the content
        // length.
        response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("application/x-x509-user-cert");
            ByteCountWriter out = new ByteCountWriter(new BufferedWriter(response.getWriter(), 8192));
            PEMWriter pemWriter = new PEMWriter(out);
        response.setContentType(CERTIFICATE_CONTENT_TYPE);
        response.setHeader("Content-Disposition",
                           "attachment; filename=" + CERTIFICATE_FILENAME);
        final ByteCountWriter out =
                new ByteCountWriter(new BufferedWriter(response.getWriter(),
                                                       8192));
        final PEMWriter pemWriter = new PEMWriter(out);

        try
        {
                pemWriter.writeObject(certkey.getChain()[0]);
                pemWriter.writeObject(certkey.getPrivateKey());

                for (int i = 1; i < certkey.getChain().length; i++)
                {
                    pemWriter.writeObject(certkey.getChain()[i]);
                }
                pemWriter.flush();
            writePEM(certificateChain, pemWriter);
        }
        finally
        {
@@ -246,11 +278,59 @@ public class ProxyServlet extends HttpServlet
            }
            catch(IOException ex)
            {
                    // TODO
                // Do nothing
            }

            logInfo.setBytes(out.getByteCount());
        }
    }

    /**
     * Write out the PEM information.
     *
     * @param certificateChain      The certificate chain to write.
     * @param pemWriter             The PEM Writer to write out to.
     * @throws IOException
     */
    void writePEM(final X509CertificateChain certificateChain,
                  final PEMWriter pemWriter) throws IOException
    {
        pemWriter.writeObject(certificateChain.getChain()[0]);
        pemWriter.writeObject(certificateChain.getPrivateKey());

        for (int i = 1; i < certificateChain.getChain().length; i++)
        {
            pemWriter.writeObject(certificateChain.getChain()[i]);
        }

        pemWriter.flush();
    }

    /**
     * Handles the HTTP <code>GET</code> method.
     * 
     * @param request servlet request
     * @param response servlet response
     * @throws java.io.IOException
     */
    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
        throws IOException
    {
        WebServiceLogInfo logInfo = new ServletLogInfo(request);
        LOGGER.info(logInfo.start());
        long start = System.currentTimeMillis();
        try
        {
            final Subject subject = getCurrentSubject(request);
            logInfo.setSubject(subject);
            
            final X509CertificateChain certificateChain =
                    getX509CertificateChain(request, subject);

            writeCertificateChain(certificateChain, response, logInfo);
        }
        catch(IllegalArgumentException ex)
        {
            logInfo.setMessage(ex.getMessage());
@@ -306,29 +386,8 @@ public class ProxyServlet extends HttpServlet
        pw.close();
    }

    /**
     * OutputStream wrapper that ensures close() is not called.
     * 
     * @author majorb
     * 
     */
    private class SafeOutputStream extends FilterOutputStream
    {
        SafeOutputStream(OutputStream ostream)
        {
            super(ostream);
        }

        @Override
        public void close() throws IOException
        {
            // do nothing
        }
    }

    public Map<X500Principal, Float> getTrustedPrincipals()
    {
        
        return Collections.unmodifiableMap(trustedPrincipals);
    }
}
+72 −12
Original line number Diff line number Diff line
@@ -34,17 +34,23 @@

package ca.nrc.cadc.cred.server;

import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.replay;
import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Map;

import javax.security.auth.x500.X500Principal;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletResponse;

import ca.nrc.cadc.auth.X509CertificateChain;
import ca.nrc.cadc.log.WebServiceLogInfo;
import org.bouncycastle.openssl.PEMWriter;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.easymock.EasyMock.*;


/**
 * Mock test of the ProxyServlet
@@ -59,9 +65,9 @@ public class ProxyServletTest
        ProxyServlet testServlet = new ProxyServlet();
        ServletConfig configMock = createMock(ServletConfig.class);
        String expectedDN1 = "cn=test1,ou=hia.nrc.ca,o=grid,c=ca";
        Float expectedDaysValid1 = new Float(30.0f);
        Float expectedDaysValid1 = 30.0f;
        String expectedDN2 = "cn=test2,ou=hia.nrc.ca,o=grid,c=ca";
        Float expectedDaysValid2 = new Float(0.5);
        Float expectedDaysValid2 = 0.5f;
        expect(configMock.getInitParameter(ProxyServlet.TRUSTED_PRINCIPALS_PARAM))
                .andReturn((expectedDN1 + '\n' + expectedDN2 + ": " + expectedDaysValid2));
        
@@ -88,7 +94,7 @@ public class ProxyServletTest
        ProxyServlet testServlet = new ProxyServlet();
        ServletConfig configMock = createMock(ServletConfig.class);
        String expectedDN1 = "cn=test1,ou=hia.nrc.ca,o=grid,c=ca";
        Float expectedDaysValid1 = new Float(-0.5);
        Float expectedDaysValid1 = -0.5f;
        expect(
                configMock
                        .getInitParameter(ProxyServlet.TRUSTED_PRINCIPALS_PARAM))
@@ -106,14 +112,68 @@ public class ProxyServletTest
        ProxyServlet testServlet = new ProxyServlet();
        ServletConfig configMock = createMock(ServletConfig.class);
        String expectedDN1 = "cn=test1,ou=hia.nrc.ca,o=grid,c=ca: WRONG FLOAT";
        expect(
                configMock
                        .getInitParameter(ProxyServlet.TRUSTED_PRINCIPALS_PARAM))
                .andReturn((expectedDN1));
        expect(configMock.getInitParameter(
                ProxyServlet.TRUSTED_PRINCIPALS_PARAM)).andReturn(expectedDN1);

        replay(configMock);

        testServlet.init(configMock);

    }

    @Test
    public void writeCertificateChain() throws Exception
    {
        final String payload = "*** MY CHAIN ***\n*** MY PRIVATE KEY ***";

        final ProxyServlet testSubject = new ProxyServlet()
        {
            /**
             * Write out the PEM information.
             *
             * @param certificateChain The certificate chain to write.
             * @param pemWriter        The PEM Writer to write out to.
             * @throws IOException
             */
            @Override
            void writePEM(final X509CertificateChain certificateChain,
                          final PEMWriter pemWriter) throws IOException
            {
                pemWriter.write(payload);
            }
        };

        final WebServiceLogInfo mockLogInfo =
                createMock(WebServiceLogInfo.class);
        final HttpServletResponse mockResponse =
                createMock(HttpServletResponse.class);
        final X509CertificateChain mockCertificateChain =
                createMock(X509CertificateChain.class);
        final Writer writer = new StringWriter();
        final PrintWriter printWriter = new PrintWriter(writer);

        mockResponse.setStatus(200);
        expectLastCall().once();

        mockResponse.setContentType(ProxyServlet.CERTIFICATE_CONTENT_TYPE);
        expectLastCall().once();

        mockResponse.setHeader("Content-Disposition",
                               "attachment; filename=cadcproxy.pem");
        expectLastCall().once();

        expect(mockResponse.getWriter()).andReturn(printWriter).once();

        mockLogInfo.setBytes(new Integer(payload.length()).longValue());
        expectLastCall().once();

        replay(mockResponse, mockLogInfo, mockCertificateChain);

        testSubject.writeCertificateChain(mockCertificateChain, mockResponse,
                                          mockLogInfo);

        assertEquals("Wrong output.", payload, writer.toString());

        verify(mockResponse, mockLogInfo, mockCertificateChain);
    }
}