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;
039
040import org.apache.commons.io.IOUtils;
041import org.eclipse.jetty.server.HttpConfiguration;
042import org.eclipse.jetty.server.HttpConnectionFactory;
043import org.eclipse.jetty.server.SecureRequestCustomizer;
044import org.eclipse.jetty.server.Server;
045import org.eclipse.jetty.server.ServerConnector;
046import org.eclipse.jetty.server.SslConnectionFactory;
047import org.eclipse.jetty.util.ssl.SslContextFactory;
048
049import dk.netarkivet.common.exceptions.IOFailure;
050import dk.netarkivet.common.utils.Settings;
051
052/**
053 * This is a registry for HTTPS remote file, meant for serving registered files to remote hosts. It will use secure
054 * communication using a shared certificate. The embedded webserver handling remote files for HTTPSRemoteFile
055 * point-to-point communication. Optimised to use direct transfer on local machine.
056 */
057public class HTTPSRemoteFileRegistry extends HTTPRemoteFileRegistry {
058
059    // Constants defining default X509 algorithm for security framework.
060    private static final String SUN_JCEKS_KEYSTORE_TYPE = "JCEKS";
061    private static final String SUN_X509_CERTIFICATE_ALGORITHM = "SunX509";
062    private static final String SSL_PROTOCOL = "SSL";
063    private static final String SHA1_PRNG_RANDOM_ALGORITHM = "SHA1PRNG";
064    /** Protocol used in this registry. */
065    private static final String PROTOCOL = "https";
066
067    /** A hostname verifier accepting any host. */
068    private static final HostnameVerifier ACCEPTING_HOSTNAME_VERIFIER = new HostnameVerifier() {
069        public boolean verify(String string, SSLSession sslSession) {
070            return true;
071        }
072    };
073
074    /** The path to the keystore containing the certificate used for SSL in HTTPS connections. */
075    private static final String KEYSTORE_PATH = Settings.get(HTTPSRemoteFile.HTTPSREMOTEFILE_KEYSTORE_FILE);
076    /** The keystore password. */
077    private static final String KEYSTORE_PASSWORD = Settings.get(HTTPSRemoteFile.HTTPSREMOTEFILE_KEYSTORE_PASSWORD);
078    /** The certificate password. */
079    private static final String KEY_PASSWORD = Settings.get(HTTPSRemoteFile.HTTPSREMOTEFILE_KEY_PASSWORD);
080
081    /**
082     * An SSL context, used for creating SSL connections only accepting this certificate.
083     */
084    private final SSLContext sslContext;
085
086    // FIXME I think this is what they call a constructor...?!
087    // This all initialises the ssl context to use the key in the keystore above.
088    private HTTPSRemoteFileRegistry() {
089        FileInputStream keyStoreInputStream = null;
090        try {
091            keyStoreInputStream = new FileInputStream(KEYSTORE_PATH);
092            KeyStore store = KeyStore.getInstance(SUN_JCEKS_KEYSTORE_TYPE);
093            store.load(keyStoreInputStream, KEYSTORE_PASSWORD.toCharArray());
094            KeyManagerFactory kmf = KeyManagerFactory.getInstance(SUN_X509_CERTIFICATE_ALGORITHM);
095            kmf.init(store, KEY_PASSWORD.toCharArray());
096            TrustManagerFactory tmf = TrustManagerFactory.getInstance(SUN_X509_CERTIFICATE_ALGORITHM);
097            tmf.init(store);
098            sslContext = SSLContext.getInstance(SSL_PROTOCOL);
099            sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(),
100                    SecureRandom.getInstance(SHA1_PRNG_RANDOM_ALGORITHM));
101        } catch (GeneralSecurityException | IOException e) {
102            throw new IOFailure("Unable to create secure environment for keystore '" + KEYSTORE_PATH + "'", e);
103        } finally {
104            IOUtils.closeQuietly(keyStoreInputStream);
105        }
106    }
107
108    /**
109     * Get the unique instance.
110     *
111     * @return The unique instance.
112     */
113    public static synchronized HTTPRemoteFileRegistry getInstance() {
114        synchronized (HTTPRemoteFile.class) {
115            if (instance == null) {
116                instance = new HTTPSRemoteFileRegistry();
117            }
118            return instance;
119        }
120    }
121
122    /**
123     * Get the protocol used for this registry, that is 'https'.
124     *
125     * @return "https", the protocol.
126     */
127    @Override
128    protected String getProtocol() {
129        return PROTOCOL;
130    }
131
132    /**
133     * Start the server, including a handler that responds with registered files, removes registered files on request,
134     * and gives 404 otherwise. Connection to this web host only possible with the shared certificate.
135     */
136    @Override
137    protected void startServer() {
138        server = new Server();
139
140        SslContextFactory sslContextFactory = new SslContextFactory();
141        sslContextFactory.setKeyStorePath(KEYSTORE_PATH);
142        sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD);
143        sslContextFactory.setKeyManagerPassword(KEY_PASSWORD);
144        sslContextFactory.setTrustStorePath(KEYSTORE_PATH);
145        sslContextFactory.setTrustStorePassword(KEYSTORE_PASSWORD);
146        sslContextFactory.setNeedClientAuth(true);
147
148        HttpConfiguration http_config = new HttpConfiguration();
149        http_config.setSecureScheme("https");
150        http_config.setSecurePort(port);
151
152        HttpConfiguration https_config = new HttpConfiguration(http_config);
153        https_config.addCustomizer(new SecureRequestCustomizer());
154
155        ServerConnector sslConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory,
156                "http/1.1"), new HttpConnectionFactory(https_config));
157        sslConnector.setPort(port);
158
159        server.addConnector(sslConnector);
160        server.setHandler(new HTTPRemoteFileRegistryHandler());
161        try {
162            server.start();
163        } catch (Exception e) {
164            throw new IOFailure("Cannot start HTTPSRemoteFile registry", e);
165        }
166    }
167
168    /**
169     * Open a connection to an URL in this registry. Thus opens SSL connections using the certificate above.
170     *
171     * @param url The URL to open connection to.
172     * @return an open connection to the given url
173     * @throws IOException If unable to open connection to the URL
174     * @throws IOFailure If the connection is not a secure connection
175     */
176    @Override
177    protected URLConnection openConnection(URL url) throws IOException {
178        URLConnection connection = url.openConnection();
179        if (!(connection instanceof HttpsURLConnection)) {
180            throw new IOFailure("Not a secure URL to remote file: " + url);
181        }
182        HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
183        httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
184        httpsConnection.setHostnameVerifier(ACCEPTING_HOSTNAME_VERIFIER);
185        return httpsConnection;
186    }
187
188}