Bug 60762 - Enhancement: Add support for runtime SNI changes in tomcat-embed
Summary: Enhancement: Add support for runtime SNI changes in tomcat-embed
Status: RESOLVED FIXED
Alias: None
Product: Tomcat 8
Classification: Unclassified
Component: Connectors (show other bugs)
Version: 8.5.x-trunk
Hardware: All All
: P2 enhancement (vote)
Target Milestone: ----
Assignee: Tomcat Developers Mailing List
URL:
Keywords:
: 55770 (view as bug list)
Depends on:
Blocks:
 
Reported: 2017-02-21 23:31 UTC by Jesse
Modified: 2018-07-27 06:44 UTC (History)
4 users (show)



Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Jesse 2017-02-21 23:31:03 UTC
This is an enhancement request, I have put it against Tomcat 8 since that is the latest stable product, if this enhancement is more appropriate for the Tomcat 9 release please move it or let me know and I can re-submit it.

The request is to add public methods for adding/removing/updating SSLHostConfig objects within the AbstractEndpoint.sslHostConfigs map.  Ideally this would be exposed publicly via the Connector class.  I think this is specific to tomcat-embed since I don't believe that runtime changes such as this are possible elsewhere.

For our application I have exposed the functionality needed by extending the NioEndpoint class, this does not consider any of the other nio2/apr/bio connectors, as we only use nio it works for us.  I have tested and been running this way for a while now.

I have also extended the AbstractHttp11JsseProtocol and Connector classes so I can gain access to my implementation of NioEndpoint, this is just for convenience, I think these methods belong publicly exposed on the Connector class (similar to how addSslHostConfig/findSslHostConfigs methods are delegated from Connector all the way to AbstractEndpoint).

The methods I've added to my NioEndpoint implementation:

    // this behaves similar to a Map.put operation, only it is void
    public void addOrUpdateSSLHostConfig(SSLHostConfig config)

    public void removeSSLHostConfig(String sniHostName)


For the nio implementation I had to ensure the ssl contexts were released/created, not sure about how that would work for the other implementations.

Thanks!
Jesse
Comment 1 Christopher Schultz 2017-02-22 04:15:52 UTC
If you call org.apache.tomcat.util.net.AbstractEndpoint.addSslHostConfig(SSLHostConfig) with a hostconfig object with a hostname matching an existing hostconfig, it will be replaced.

Is this not sufficient?

Note that the new SSLHostConfig has no effect unless you bounce the whole connector.
Comment 2 Jesse 2017-02-22 17:47:34 UTC
In version 8.5.9 that we are running it looks like that method calls putIfAbsent against the sslHostConfigs map, throwing an IllegalArgumentException if there is a duplicate SSLHostConfig object for the given key/hostname.  From what I can tell there is no existing public method in 8.5.9 to modify an existing SSLHostConfig once loaded, additionally the methods to do so properly with regard to ssl context release/create are protected.  Please correct me if my understanding of this is wrong.

Also, in the case where an SSL certificate or SSLHostConfig object needs to be removed for any reason, this method would not suffice with either behavior.

When you say that the new SSLHostConfig has no effect without bouncing the whole connector, is this in a newer tomcat version?  Or do you refer to the newly created SSLHostConfig object that we are creating and putting in the sslHostConfigs map?  In the latter case we definitely are seeing the newly created SSLHostConfig object taking effect and new requests to it's hostname are being served the corresponding certificate.
Comment 3 Mark Thomas 2017-03-06 22:08:07 UTC
For anyone looking at implementing this, the following thread will be useful:
http://markmail.org/thread/ox3h7oaqgef3qqyk

The short version is removing an SSLHostConfig for APR/native is going to be  tricky.
Comment 4 Ralf Hauser 2017-07-21 07:41:10 UTC
Might be useful to implement letsencrypt - see also

http://people.apache.org/~schultz/ApacheCon NA 2017/Let's Encrypt Apache Tomcat.pdf
Comment 5 Jesse 2017-07-21 18:23:16 UTC
We have actually implemented letsencrypt in our application.  We allow users to configure new domains that our application will respond to over SSL, and we give them the option to provide their own certificate or the application can generate (and auto-renew) for them via letsencrypt.

I do agree that this request would align well with the needs of tomcat supporting letsencrypt, although my request is actually to provide the public methods so our application can control things as necessary to leverage letsencrypt or user provided certificates at runtime.  Those same methods could likely be used by more generic letsencrypt support as well.
Comment 6 Mark Thomas 2017-09-15 20:12:24 UTC
This has been implemented in 9.0.x for 9.0.0.M28 onwards.

I'll back-port it to 8.5.x once folks have had a chance to test it.
Comment 7 Mark Thomas 2017-11-23 15:10:40 UTC
*** Bug 55770 has been marked as a duplicate of this bug. ***
Comment 8 Mark Thomas 2017-11-23 18:51:28 UTC
This will be available in 8.5.24 onwards.

There are no plans at this time to back-port this to 8.0.x or 7.0.x.
Comment 9 Jesse 2017-12-05 07:03:13 UTC
Hi Mark,

Thanks for your work on this, I see the methods on AbstractEndpoint and I think they are exactly what I need and should allow me to remove my custom implementation.

The only thing holding me back at this point is that I don't know how to get access to the AbstractEndpoint from our application.  We have an instance of Tomcat and can get a Connector using the getConnector method, from there I can get the ProtocolHandler but that does not offer any way to get the endpoint.  I could probably safely cast ProtocolHandler to AbstractProtocol where there is a getEndpoint method, but that method is protected.

Is there an appropriate way for me to get access to the new public methods on AbstractEndpoint?

Thanks again!
Jesse
Comment 10 Mark Thomas 2017-12-05 14:19:23 UTC
Off the top of my head (I can check later if this doesn't work) you should be able to do this via JMX.
Comment 11 Jesse 2017-12-05 18:03:26 UTC
I'd rather not use JMX, I'm not totally familiar with JMX but wouldn't that require me to enable JMX on a port in our application?

If I can use JMX directly via the embedded tomcat without doing any tcp port type access then that would work.

For now I have been able to remove my implementation of methods in NioEndpoint in favor of using the ones you have, but I still have my own extension classes so I can get access to them.

Thanks,
Jesse
Comment 12 Christopher Schultz 2017-12-05 19:16:47 UTC
(In reply to Jesse from comment #11)
> I'd rather not use JMX, I'm not totally familiar with JMX but wouldn't that
> require me to enable JMX on a port in our application?

You can use JMX within the same JVM just via an API interface. No need to expose any ports or anything like that.
Comment 13 Jesse 2017-12-05 20:15:07 UTC
I was able to see many tomcat mbeans using:

ManagementFactory.getPlatformMBeanServer().queryNames(new ObjectName("Tomcat:*"), null)

However I don't see any that look like they'd represent an endpoint in that list.

I also looked for "Catalina:*" but none were found.

Given that I am running this in the same JVM as the Tomcat instance that I want to manage, if I could find an ObjectName that represents the AbstractEndpoint, could I actually get access to the AbstractEndpoint that I want to invoke the add/removeSslHostConfig methods on?  It seems like the JMX interfaces are all for string/serialized communications and I need to pass the SSLHostConfig object that I've created.

Thanks!
Comment 14 Mark Thomas 2017-12-05 20:30:19 UTC
Catalina:type=ThreadPool,name="http-nio-8080"
Comment 15 Jesse 2017-12-05 21:05:39 UTC
That gives an InstanceNotFoundException, I can't find any Catalina:* objects.

Even if that did return an object, could I gain direct access to the AbstractEndpoint instance that I want to call the methods against?

Thanks!
Comment 16 Mark Thomas 2017-12-05 21:16:18 UTC
Please move this to the users list.
Comment 17 Mahesh 2018-07-20 05:33:24 UTC
Hello,

I was facing this exact bug and glad it is resolved.

But I'm not sure how to proceed with this (really new to configuring tomcat).

I've configured everything in server.xml and 2 way handshaking works. But how do I periodically call the method added to solve this bug ?

Also I'm not sure which method was actually added. How do I find the code commit related to this bug ? I tried to find it in github repo but with no success.

Any help will be really appreciated.

Thanks.
Comment 18 Mahesh 2018-07-20 08:09:04 UTC
I guess I found the method.

It's reloadSslHostConfig's 2 variants.

But how do I call it ?
Comment 19 Mark Thomas 2018-07-21 10:46:53 UTC
The Tomcat community does not use Bugzilla as a user support forum. Questions relating to the usage of Apache Tomcat are very unlikely to receive an answer here and should be directed to the Apache Tomcat users mailing list.
Comment 20 Mahesh 2018-07-27 06:44:08 UTC
Sure. I could find some ways myself. Posting them here for anyone who comes across this.

There is now a solution to this starting with Tomcat v8.5.24.

They introduced 2 methods named:

reloadSslHostConfig(String hostName) - to reload a specific host
reloadSslHostConfigs() - reload all

They can be called in various ways:

 1. Using jmx
 2. Using manager service
 3. By making custom protocol - I found this way during my research

Details of way 1 and way 2 are easily available online.

Details of how to go about using way 3:

 1. Make a class extending the protocol of your choice for eg. Http11NioProtocol
 2. Override the required methods and just call super in them to keep default behavior 
 3. Make a thread in this class to call reloadSslHostConfigs method time to time
 4. Package this class in a jar and put that jar in tomcat's lib folder
 5. Edit protocol in connector in server.xml to use this custom defined protocol 

Find sample code below:

Main protocol class:

        package com.myown.connector;
        
        import java.io.File;
        import java.io.InputStream;
        import java.lang.reflect.Field;
        import java.net.URL;
        import java.net.URLConnection;
        import java.nio.file.StandardCopyOption;
        import java.util.ArrayList;
        import java.util.List;
        import java.util.concurrent.ConcurrentMap;
        
        import javax.management.MalformedObjectNameException;
        import javax.management.ObjectName;
        import javax.net.ssl.SSLSessionContext;
        
        import org.apache.coyote.http11.Http11NioProtocol;
        import org.apache.juli.logging.Log;
        import org.apache.juli.logging.LogFactory;
        import org.apache.tomcat.util.modeler.Registry;
        import org.apache.tomcat.util.net.AbstractEndpoint;
        import org.apache.tomcat.util.net.AbstractJsseEndpoint;
        import org.apache.tomcat.util.net.GetSslConfig;
        import org.apache.tomcat.util.net.SSLContext;
        import org.apache.tomcat.util.net.SSLHostConfig;
        import org.apache.tomcat.util.net.SSLHostConfigCertificate;
        import org.apache.tomcat.util.net.SSLImplementation;
        import org.apache.tomcat.util.net.SSLUtil;
        
        public class ReloadProtocol extends Http11NioProtocol {
         	
        	private static final Log log = LogFactory.getLog(Http12ProtocolSSL.class);
        
        	public ReloadProtocol() {
        		super();
        		RefreshSslConfigThread refresher = new 
                      RefreshSslConfigThread(this.getEndpoint(), this);
        		refresher.start();
        	}
        
        	@Override
        	public void setKeystorePass(String s) {
        		super.setKeystorePass(s);
        	}
        
        	@Override
        	public void setKeyPass(String s) {
        		super.setKeyPass(s);
        	}
        
        	@Override
        	public void setTruststorePass(String p) {
        		super.setTruststorePass(p);
        	}
        
        	class RefreshSslConfigThread extends Thread {
        
        		AbstractJsseEndpoint<?> abstractJsseEndpoint = null;
        		Http11NioProtocol protocol = null;
        
        		public RefreshSslConfigThread(AbstractJsseEndpoint<?> abstractJsseEndpoint, Http11NioProtocol protocol) {
        			this.abstractJsseEndpoint = abstractJsseEndpoint;
        			this.protocol = protocol;
        		}
        
        		public void run() {
        			int timeBetweenRefreshesInt = 1000000; // time in milli-seconds
        			while (true) {
        				try {
                                abstractJsseEndpoint.reloadSslHostConfigs();
        						System.out.println("Config Updated");
        				} catch (Exception e) {
        					System.out.println("Problem while reloading.");
        				}
        				try {
        					Thread.sleep(timeBetweenRefreshesInt);
        				} catch (InterruptedException e) {
        					System.out.println("Error while sleeping");
        				}
        			}
        		}
           }
    }



Connector in server.xml should mention this as the protocol:

    <Connector protocol="com.myown.connector.ReloadProtocol"
     ..........


Hope this helps.