/*
* Copyright (C) 2012-2016 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.facebook.nifty.ssl;
import com.facebook.nifty.core.NettyServerTransport;
import com.google.common.collect.ImmutableSet;
import io.airlift.log.Logger;
import org.apache.tomcat.jni.SessionTicketKey;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Objects.requireNonNull;
/**
* Watches SSL config files for changes and updates the {@link SslServerConfiguration} of the attached
* {@link NettyServerTransport} when the files change. Three kind of files are watched:
* <ul>
* <li>a ticket seed file. New ticket seeds are constructed from it with a {@link TicketSeedFileParser}.</li>
* <li>a TLS private key file. Passed to the SslServerConfiguration, which loads the key from the file.</li>
* <li>a TLS certificate file. Passed to the SslServerConfiguration, which loads the cert from the file.</li>
* </ul>
* Note that the file paths cannot be changed after the watcher is created. The watcher will automatically start
* polling files for changes when it is attached to a NettyServerTransport, and will stop polling when it is
* detached from the transport. All three file paths are currently required.
*/
public class SslConfigFileWatcher implements TransportAttachObserver {
private final AtomicReference<NettyServerTransport> transportRef;
private final File ticketSeedFile;
private final File keyFile;
private final File certFile;
private final TicketSeedFileParser ticketSeedFileParser;
private final MultiFileWatcher watcher;
private static final Logger log = Logger.get(PollingMultiFileWatcher.class);
/**
* Constructs a new config file watcher.
*
* @param ticketSeedFile the path to the ticket seed file. May not be null.
* @param keyFile the path to the TLS private key file. May not be null.
* @param certFile the path to the TLS certificate file. May not be null.
* @param ticketSeedSalt the ticket seed salt. If null, uses {@link TicketSeedFileParser#DEFAULT_TICKET_SALT}.
* @param watcher the {@link MultiFileWatcher} to use for monitoring SSL config files for changes.
*/
public SslConfigFileWatcher(File ticketSeedFile,
File keyFile,
File certFile,
byte[] ticketSeedSalt,
MultiFileWatcher watcher) {
transportRef = new AtomicReference<>(null);
this.ticketSeedFile = requireNonNull(ticketSeedFile);
this.keyFile = requireNonNull(keyFile);
this.certFile = requireNonNull(certFile);
ticketSeedFileParser = new TicketSeedFileParser(ticketSeedSalt);
this.watcher = requireNonNull(watcher);
}
@Override
public void attachTransport(NettyServerTransport transport) {
log.debug("Attaching %s observer to %s",
getClass().getSimpleName(),
requireNonNull(transport).getClass().getSimpleName());
this.transportRef.set(transport);
watcher.start(ImmutableSet.of(ticketSeedFile, keyFile, certFile), this::onFilesUpdated);
}
@Override
public void detachTransport() {
NettyServerTransport transport = requireNonNull(transportRef.get());
log.debug("Detaching %s observer from %s",
getClass().getSimpleName(),
transport.getClass().getSimpleName());
watcher.shutdown();
this.transportRef.set(null);
}
private void onFilesUpdated(Set<File> modifiedFiles) {
log.debug("%s.onFilesUpdated(modifiedFiles = %s)",
getClass().getSimpleName(),
requireNonNull(modifiedFiles));
NettyServerTransport transport = requireNonNull(transportRef.get());
boolean ticketSeedFileUpdated = modifiedFiles.contains(ticketSeedFile);
boolean keyFileUpdated = modifiedFiles.contains(keyFile);
boolean certFileUpdated = modifiedFiles.contains(certFile);
boolean needUpdate = ticketSeedFileUpdated || keyFileUpdated || certFileUpdated;
while (needUpdate) {
log.debug("Trying to update server configuration ...");
SslServerConfiguration oldConfig = transport.getSSLConfiguration();
SslServerConfiguration.BuilderBase<?> builder;
boolean isOpenSsl = false;
if (oldConfig instanceof OpenSslServerConfiguration) {
builder = OpenSslServerConfiguration.newBuilder();
isOpenSsl = true;
}
else {
builder = JavaSslServerConfiguration.newBuilder();
}
builder.initFromConfiguration(oldConfig);
if (ticketSeedFileUpdated && isOpenSsl) {
// Note: JavaSslServerConfiguration does not currently support ticket keys, so only update them
// if using the OpenSSL implementation.
OpenSslServerConfiguration.Builder openSslBuilder = (OpenSslServerConfiguration.Builder) builder;
try {
List<SessionTicketKey> ticketKeys = ticketSeedFileParser.parse(ticketSeedFile);
openSslBuilder.ticketKeys(ticketKeys.toArray(new SessionTicketKey[ticketKeys.size()]));
}
catch (IOException e) {
throw new RuntimeException(e);
}
}
if (keyFileUpdated) {
builder.keyFile(keyFile);
}
if (certFileUpdated) {
builder.certFile(certFile);
}
SslServerConfiguration newConfig = builder.createServerConfiguration();
needUpdate = !transport.compareAndSetSSLConfiguration(oldConfig, newConfig);
if (!needUpdate) {
log.debug("Update succeeded!");
}
else {
log.debug("Update failed!");
}
}
}
}