001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.common.distribute;
024
025import java.io.FileInputStream;
026import java.io.IOException;
027import java.net.URL;
028import java.net.URLConnection;
029import java.security.GeneralSecurityException;
030import java.security.KeyStore;
031import java.security.SecureRandom;
032
033import javax.net.ssl.HostnameVerifier;
034import javax.net.ssl.HttpsURLConnection;
035import javax.net.ssl.KeyManagerFactory;
036import javax.net.ssl.SSLContext;
037import javax.net.ssl.SSLSession;
038import javax.net.ssl.TrustManagerFactory;
039import org.mortbay.jetty.Server;
040import org.mortbay.jetty.security.SslSocketConnector;
041import org.apache.commons.io.IOUtils;
042
043import dk.netarkivet.common.exceptions.IOFailure;
044import dk.netarkivet.common.utils.Settings;
045
046/**
047 * This is a registry for HTTPS remote file, meant for serving registered files to remote hosts. It will use secure
048 * communication using a shared certificate. The embedded webserver handling remote files for HTTPSRemoteFile
049 * point-to-point communication. Optimised to use direct transfer on local machine.
050 */
051public class HTTPSRemoteFileRegistry extends HTTPRemoteFileRegistry {
052
053    // Constants defining default X509 algorithm for security framework.
054    private static final String SUN_JCEKS_KEYSTORE_TYPE = "JCEKS";
055    private static final String SUN_X509_CERTIFICATE_ALGORITHM = "SunX509";
056    private static final String SSL_PROTOCOL = "SSL";
057    private static final String SHA1_PRNG_RANDOM_ALGORITHM = "SHA1PRNG";
058    /** Protocol used in this registry. */
059    private static final String PROTOCOL = "https";
060
061    /** A hostname verifier accepting any host. */
062    private static final HostnameVerifier ACCEPTING_HOSTNAME_VERIFIER = new HostnameVerifier() {
063        public boolean verify(String string, SSLSession sslSession) {
064            return true;
065        }
066    };
067
068    /** The path to the keystore containing the certificate used for SSL in HTTPS connections. */
069    private static final String KEYSTORE_PATH = Settings.get(HTTPSRemoteFile.HTTPSREMOTEFILE_KEYSTORE_FILE);
070    /** The keystore password. */
071    private static final String KEYSTORE_PASSWORD = Settings.get(HTTPSRemoteFile.HTTPSREMOTEFILE_KEYSTORE_PASSWORD);
072    /** The certificate password. */
073    private static final String KEY_PASSWORD = Settings.get(HTTPSRemoteFile.HTTPSREMOTEFILE_KEY_PASSWORD);
074
075    /**
076     * An SSL context, used for creating SSL connections only accepting this certificate.
077     */
078    private final SSLContext sslContext;
079
080    // This all initialises the ssl context to use the key in the keystore above.
081    private HTTPSRemoteFileRegistry() {
082        FileInputStream keyStoreInputStream = null;
083        try {
084            keyStoreInputStream = new FileInputStream(KEYSTORE_PATH);
085            KeyStore store = KeyStore.getInstance(SUN_JCEKS_KEYSTORE_TYPE);
086            store.load(keyStoreInputStream, KEYSTORE_PASSWORD.toCharArray());
087            KeyManagerFactory kmf = KeyManagerFactory.getInstance(SUN_X509_CERTIFICATE_ALGORITHM);
088            kmf.init(store, KEY_PASSWORD.toCharArray());
089            TrustManagerFactory tmf = TrustManagerFactory.getInstance(SUN_X509_CERTIFICATE_ALGORITHM);
090            tmf.init(store);
091            sslContext = SSLContext.getInstance(SSL_PROTOCOL);
092            sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
093                    SecureRandom.getInstance(SHA1_PRNG_RANDOM_ALGORITHM));
094        } catch (GeneralSecurityException | IOException e) {
095            throw new IOFailure("Unable to create secure environment for keystore '" + KEYSTORE_PATH + "'", e);
096        } finally {
097            IOUtils.closeQuietly(keyStoreInputStream);
098        }
099    }
100
101    /**
102     * Get the unique instance.
103     *
104     * @return The unique instance.
105     */
106    public static synchronized HTTPRemoteFileRegistry getInstance() {
107        synchronized (HTTPRemoteFile.class) {
108            if (instance == null) {
109                instance = new HTTPSRemoteFileRegistry();
110            }
111            return instance;
112        }
113    }
114
115    /**
116     * Get the protocol used for this registry, that is 'https'.
117     *
118     * @return "https", the protocol.
119     */
120    @Override
121    protected String getProtocol() {
122        return PROTOCOL;
123    }
124    
125    /**
126     * Start the server, including a handler that responds with registered files, removes registered files on request,
127     * and gives 404 otherwise. Connection to this web host only possible with the shared certificate.
128     */
129    protected void startServer() {
130        server = new Server();
131        //This sets up a secure connector
132        SslSocketConnector connector = new SslSocketConnector();
133        connector.setKeystore(KEYSTORE_PATH);
134        connector.setPassword(KEYSTORE_PASSWORD);
135        connector.setKeyPassword(KEY_PASSWORD);
136        connector.setTruststore(KEYSTORE_PATH);
137        connector.setTrustPassword(KEYSTORE_PASSWORD);
138        connector.setNeedClientAuth(true);
139        connector.setPort(port);
140        //This initialises the server.
141        server.addConnector(connector);
142        server.addHandler(new HTTPRemoteFileRegistryHandler());
143        try {
144            server.start();
145        } catch (Exception e) {
146            throw new IOFailure("Cannot start HTTPSRemoteFile registry", e);
147        }
148    }
149
150    /**
151     * Open a connection to an URL in this registry. Thus opens SSL connections using the certificate above.
152     *
153     * @param url The URL to open connection to.
154     * @return an open connection to the given url
155     * @throws IOException If unable to open connection to the URL
156     * @throws IOFailure If the connection is not a secure connection
157     */
158    @Override
159    protected URLConnection openConnection(URL url) throws IOException {
160        URLConnection connection = url.openConnection();
161        if (!(connection instanceof HttpsURLConnection)) {
162            throw new IOFailure("Not a secure URL to remote file: " + url);
163        }
164        HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
165        httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
166        httpsConnection.setHostnameVerifier(ACCEPTING_HOSTNAME_VERIFIER);
167        return httpsConnection;
168    }
169
170}