Skip to content

Commit

Permalink
Merge pull request #177 from camlow325/feature/1.x/TK-149-runtime-crl…
Browse files Browse the repository at this point in the history
…-refresh

(TK-149) Reload the crl file in the running webservers on change
  • Loading branch information
Moses Mendoza committed Jul 18, 2017
2 parents 11553f7 + 45ef1e4 commit a9aaf57
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 15 deletions.
1 change: 1 addition & 0 deletions examples/ring_app/bootstrap.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
puppetlabs.trapperkeeper.services.nrepl.nrepl-service/nrepl-service
puppetlabs.trapperkeeper.services.webserver.jetty9-service/jetty9-service
puppetlabs.trapperkeeper.services.watcher.filesystem-watch-service/filesystem-watch-service
examples.ring-app.example-services/count-service
examples.ring-app.example-services/bert-service
examples.ring-app.example-services/ernie-service
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.puppetlabs.trapperkeeper.services.webserver.jetty9.utils;

import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.security.CertificateUtils;
import org.eclipse.jetty.util.ssl.SslContextFactory;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLSocket;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.cert.CRL;
import java.util.Collection;

public class InternalSslContextFactory extends SslContextFactory {

private static int maxTries = 25;
private static int sleepInMillisecondsBetweenTries = 100;
private static final Logger LOG =
Log.getLogger(InternalSslContextFactory.class);

private Collection<? extends CRL> _crls;

@Override
protected void checkNotStarted() {
}

@Override
protected void doStart() throws Exception {
synchronized (this) {
load();
}
}

@Override
protected void doStop() throws Exception {
synchronized (this) {
unload();
}
super.doStop();
}

@Override
protected Collection<? extends CRL> loadCRL(String crlPath) throws Exception {
Collection<? extends CRL> crls;

synchronized (this) {
if (_crls == null) {
crls = super.loadCRL(crlPath);
} else {
crls = _crls;
}
}

return crls;
}

private void load() throws Exception {
synchronized (this) {
super.doStart();
}
}

private void unload() throws Exception {
synchronized (this) {
super.doStop();
}
}

@Override
public SSLContext getSslContext() {
synchronized (this) {
return super.getSslContext();
}
}

@Override
public SSLServerSocket newSslServerSocket(String host,
int port,
int backlog)
throws IOException {
synchronized (this) {
return super.newSslServerSocket(host, port, backlog);
}
}

@Override
public SSLSocket newSslSocket() throws IOException {
synchronized (this) {
return super.newSslSocket();
}
}

@Override
public SSLEngine newSSLEngine() {
synchronized (this) {
return super.newSSLEngine();
}
}

@Override
public SSLEngine newSSLEngine(String host, int port) {
synchronized (this) {
return super.newSSLEngine(host, port);
}
}

@Override
public SSLEngine newSSLEngine(InetSocketAddress address) {
synchronized (this) {
return super.newSSLEngine(address);
}
}

public void reload() throws Exception {
synchronized (this) {
Exception reloadEx = null;
int tries = maxTries;
String crlPath = getCrlPath();

if (crlPath != null) {
File crlPathAsFile = new File(crlPath);
long crlLastModified = crlPathAsFile.lastModified();

// Try to parse CRLs from the crlPath until it is successful
// or a hard-coded number of failed attempts have been made.
do {
reloadEx = null;
try {
_crls = CertificateUtils.loadCRL(crlPath);
} catch (Exception e) {
reloadEx = e;

// If the CRL file has been updated since the last reload
// attempt, reset the retry counter.
if (crlPathAsFile != null &&
crlLastModified != crlPathAsFile.lastModified()) {
crlLastModified = crlPathAsFile.lastModified();
tries = maxTries;
} else {
tries--;
}

if (tries == 0) {
LOG.warn("Failed ssl context reload after " +
maxTries + " tries. CRL file is: " +
crlPath, reloadEx);
} else {
Thread.sleep(sleepInMillisecondsBetweenTries);
}
}
} while (reloadEx != null && tries > 0);
}

if (reloadEx == null) {
unload();
load();
}
}
}
}
1 change: 1 addition & 0 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
[puppetlabs/kitchensink]
[puppetlabs/trapperkeeper]
[puppetlabs/i18n]
[puppetlabs/trapperkeeper-filesystem-watcher]
]

:source-paths ["src"]
Expand Down
31 changes: 27 additions & 4 deletions src/puppetlabs/trapperkeeper/services/webserver/jetty9_core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
(java.lang.management ManagementFactory)
(org.eclipse.jetty.jmx MBeanContainer)
(org.eclipse.jetty.util URIUtil BlockingArrayQueue)
(java.io IOException))
(java.io IOException File)
(com.puppetlabs.trapperkeeper.services.webserver.jetty9.utils InternalSslContextFactory))

(:require [ring.util.servlet :as servlet]
[ring.util.codec :as codec]
Expand All @@ -33,8 +34,11 @@
[puppetlabs.trapperkeeper.services.webserver.experimental.jetty9-websockets :as websockets]
[puppetlabs.trapperkeeper.services.webserver.normalized-uri-helpers
:as normalized-uri-helpers]
[puppetlabs.trapperkeeper.services.protocols.filesystem-watch-service
:as watch-protocol]
[schema.core :as schema]
[puppetlabs.i18n.core :as i18n]))
[puppetlabs.i18n.core :as i18n]
[me.raynes.fs :as fs]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; JDK SecurityProvider Hack
Expand Down Expand Up @@ -137,7 +141,7 @@
:overrides-read-by-webserver schema/Bool
:overrides (schema/maybe {schema/Keyword schema/Any})
:endpoints RegisteredEndpoints
:ssl-context-factory (schema/maybe SslContextFactory)})
:ssl-context-factory (schema/maybe InternalSslContextFactory)})

(def ServerContext
{:state (schema/atom ServerContextState)
Expand Down Expand Up @@ -188,7 +192,7 @@
(if (some #(= "sslv3" %) (map str/lower-case protocols))
(log/warn (i18n/trs "`ssl-protocols` contains SSLv3, a protocol with known vulnerabilities; we recommend removing it from the `ssl-protocols` list")))

(let [context (doto (SslContextFactory.)
(let [context (doto (InternalSslContextFactory.)
(.setKeyStore (:keystore keystore-config))
(.setKeyStorePassword (:key-password keystore-config))
(.setTrustStore (:truststore keystore-config))
Expand Down Expand Up @@ -952,6 +956,25 @@
[server-id (initialize-context)]))
:default-server (get-default-server-from-config config)))

(schema/defn ^:always-validate reload-crl-on-change!
"Reload the CRL file used by the supplied ssl-context-factory whenever any
changes to the file occur."
[ssl-context-factory :- InternalSslContextFactory
watcher :- (schema/protocol watch-protocol/Watcher)]
(when-let [crl-path (.getCrlPath ssl-context-factory)]
(let [normalized-crl-path (.getCanonicalPath (fs/file crl-path))]
(watch-protocol/add-watch-dir! watcher (fs/parent crl-path)
{:recursive true})
(watch-protocol/add-callback!
watcher
(fn [events]
(when (some #(and
(:changed-path %)
(= (.getCanonicalPath (:changed-path %))
normalized-crl-path))
events)
(.reload ssl-context-factory)))))))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Service Function Implementations

Expand Down
42 changes: 32 additions & 10 deletions src/puppetlabs/trapperkeeper/services/webserver/jetty9_service.clj
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
(ns puppetlabs.trapperkeeper.services.webserver.jetty9-service
(:import (org.eclipse.jetty.jmx MBeanContainer))
(:import (org.eclipse.jetty.jmx MBeanContainer)
(java.io File))
(:require
[clojure.tools.logging :as log]

[puppetlabs.trapperkeeper.services.webserver.jetty9-config :as config]
[puppetlabs.trapperkeeper.services.webserver.jetty9-core :as core]
[puppetlabs.trapperkeeper.services :refer [service-context]]
[puppetlabs.trapperkeeper.services.protocols.filesystem-watch-service
:as watch-protocol]
[puppetlabs.trapperkeeper.services :refer [get-service
maybe-get-service
service-context]]
[puppetlabs.trapperkeeper.config :as tk-config]
[puppetlabs.trapperkeeper.core :refer [defservice]]
[puppetlabs.i18n.core :as i18n]))
[puppetlabs.i18n.core :as i18n]
[me.raynes.fs :as fs]))

;; TODO: this should probably be moved to a separate jar that can be used as
;; a dependency for all webserver service implementations
Expand All @@ -26,23 +33,38 @@
(defservice jetty9-service
"Provides a Jetty 9 web server as a service"
WebserverService
[[:ConfigService get-in-config]]
{:required [ConfigService]
:optional [FilesystemWatchService]}
(init [this context]
(log/info (i18n/trs "Initializing web server(s)."))
(let [config (or (get-in-config [:webserver])
(let [config-service (get-service this :ConfigService)
config (or (tk-config/get-in-config config-service [:webserver])
;; Here for backward compatibility with existing projects
(get-in-config [:jetty])
(tk-config/get-in-config config-service [:jetty])
{})]
(config/validate-config config)
(core/init! context config)))

(start [this context]
(log/info (i18n/trs "Starting web server(s)."))
(let [config (or (get-in-config [:webserver])
(let [config-service (get-service this :ConfigService)
config (or (tk-config/get-in-config config-service [:webserver])
;; Here for backward compatibility with existing projects
(get-in-config [:jetty])
{})]
(core/start! context config)))
(tk-config/get-in-config config-service [:jetty])
{})
started-context (core/start! context config)]
(if-let [filesystem-watcher-service
(maybe-get-service this :FilesystemWatchService)]
(let [watcher (watch-protocol/create-watcher filesystem-watcher-service)]
(doseq [server (:jetty9-servers started-context)]
(when-let [ssl-context-factory (-> server
second
:state
deref
:ssl-context-factory)]
(core/reload-crl-on-change! ssl-context-factory watcher)))
(assoc started-context :watcher watcher))
started-context)))

(stop [this context]
(log/info (i18n/trs "Shutting down web server(s)."))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
[puppetlabs.trapperkeeper.services :as tk-services]
[puppetlabs.trapperkeeper.services.webserver.jetty9-service
:refer :all]
[puppetlabs.trapperkeeper.services.watcher.filesystem-watch-service
:as filesystem-watch-service]
[puppetlabs.trapperkeeper.testutils.webserver :as testutils]
[puppetlabs.trapperkeeper.testutils.webserver.common :refer :all]
[puppetlabs.trapperkeeper.testutils.bootstrap
Expand All @@ -25,7 +27,9 @@
[puppetlabs.trapperkeeper.testutils.logging :as tk-log-testutils]
[puppetlabs.trapperkeeper.services.webserver.jetty9-core :as core]
[schema.core :as schema]
[schema.test :as schema-test]))
[schema.test :as schema-test]
[puppetlabs.kitchensink.core :as ks]
[me.raynes.fs :as fs]))

(use-fixtures :once
ks-test-fixtures/with-no-jvm-shutdown-hooks
Expand Down Expand Up @@ -372,6 +376,55 @@
[:webserver :ssl-crl-path]
"./dev-resources/config/jetty/ssl/crls/crls_bogus.pem")))))))

(deftest crl-reloaded-without-server-restart-test
(let [tmp-dir (ks/temp-dir)
tmp-file (fs/file tmp-dir "mycrl.pem")
get-request #(http-get
(str "https://localhost:8081" hello-path)
default-options-for-https-client)]
(fs/copy "./dev-resources/config/jetty/ssl/crls/crls_none_revoked.pem"
tmp-file)
(with-app-with-config
app
[jetty9-service
hello-webservice
filesystem-watch-service/filesystem-watch-service]

(assoc-in
jetty-ssl-client-need-config
[:webserver :ssl-crl-path]
(str tmp-file))

(testing "request to jetty successful before cert revoked"
(let [response (get-request)]
(is (= (:status response) 200))
(is (= (:body response) hello-body))))

(testing "request fails after cert revoked"
;; Sleep a bit to wait for the file watcher to be ready to poll/notify
;; for change events on the CRL. Ideally wouldn't have to do this but
;; seems like the initial event notification doesn't propagate if the
;; file is changed too soon after initialization.
(Thread/sleep 1000)
(fs/copy "./dev-resources/config/jetty/ssl/crls/crls_localhost_revoked.pem"
tmp-file)
(is
(loop [times 30]
(cond
(try
(ssl-exception-thrown? (get-request))
(catch IllegalStateException _
false))
true

(zero? times)
(ssl-exception-thrown? (get-request))

:else
(do
(Thread/sleep 500)
(recur (dec times))))))))))

(defn boot-service-and-jetty-with-default-config
[service]
(tk-core/boot-services-with-config
Expand Down

0 comments on commit a9aaf57

Please sign in to comment.