Enable SSL and add certificate programatically in Embedded Apache Tomcat 9 - java

I have the following code to start my software:
public static void main(String[] args) throws Exception {
// set system property for exit on failure
System.setProperty("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true");
// create tomcat
Tomcat tomcat = new Tomcat();
// create connector, configure and add to tomcat
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setMaxPostSize(-1);
connector.setPort(8080);
connector.setURIEncoding("UTF-8");
((Http11NioProtocol)connector.getProtocolHandler()).setConnectionUploadTimeout(36000000);
((Http11NioProtocol)connector.getProtocolHandler()).setDisableUploadTimeout(false);
((Http11NioProtocol)connector.getProtocolHandler()).setConnectionTimeout(3600000);
((Http11NioProtocol)connector.getProtocolHandler()).setCompression("on");
((Http11NioProtocol)connector.getProtocolHandler()).setCompressibleMimeType("text/html,text/xml,text/plain,application/javascript");
tomcat.setConnector(connector);
// add web app with jsps and servlets
StandardContext standardContext = (StandardContext)tomcat.addWebapp("", new File(".").getAbsolutePath()+"/src/webroot");
standardContext.getJarScanner().setJarScanFilter(new JarScanFilter() { #Override public boolean check(JarScanType jarScanType, String s) {
if(s != null){
if(s.startsWith("mchange-commons-java")){
return false;
}
}
return true;
}});
standardContext.setParentClassLoader(Run.class.getClassLoader());
WebResourceRoot webResourceRoot = new StandardRoot(standardContext);
File additionWebInfClassesFolder = new File(new File(".").getAbsolutePath(), "target/classes");
WebResourceSet webResourceSet = new DirResourceSet(webResourceRoot, "/WEB-INF/classes", additionWebInfClassesFolder.getAbsolutePath(), "/");
webResourceRoot.addPreResources(webResourceSet);
standardContext.setResources(webResourceRoot);
// start tomcat
tomcat.start();
// stay in this method as long as tomcat is running
tomcat.getServer().await();
}
Now I have my certificate files (private key, certificate) and I want to add SSL functionality to this Tomcat Server. I know that this might not be best practice, but I am looking for a very simple way to do that. I know I can create a keystore file and add the properties to the connector but what I basically want is to have a string with my certificate content and apply that.

My solution winds up looking a lot like the code I finally stumbled upon here to help me fix my issues: https://github.com/OryxProject/oryx/blob/master/framework/oryx-lambda-serving/src/main/java/com/cloudera/oryx/lambda/serving/ServingLayer.java#L202
Note: I believe I am using Tomcat 10.
private Connector createSslConnector(){
Connector httpsConnector = new Connector();
httpsConnector.setPort(443);
httpsConnector.setSecure(true);
httpsConnector.setScheme("https");
httpsConnector.setAttribute("SSLEnabled", "true");
SSLHostConfig sslConfig = new SSLHostConfig();
SSLHostConfigCertificate certConfig = new SSLHostConfigCertificate(sslConfig, SSLHostConfigCertificate.Type.RSA);
certConfig.setCertificateKeystoreFile("/root/.keystore");
certConfig.setCertificateKeystorePassword("changeit");
certConfig.setCertificateKeyAlias("mykeyalias");
sslConfig.addCertificate(certConfig);
httpsConnector.addSslHostConfig(sslConfig);
return httpsConnector;
}

Related

How to force Spring boot embedded tomcat to use SNI (Server Name Indication)

I need to make sure that my client supports SNI and it works. To do that, I want to start some dummy server (Spring boot application with it's embedded Tomcat 9) that requires hostname from client. The question is how to force Spring boot embedded Tomcat to require SNI from client?
I've read that to do that, we need to provide to server 2 (or more) certificates.
I tried to follow example from this topic. But it does not work for me.
I generated 2 keystores mycompany1.keystore and mycompany2.keystore with 1 certificate in each of them. And 1 truststore mycompany.truststore that stores both these certificates. Certificates have CN my.hostname.com and my.another.hostname.com.
This is the way how I configure embedded tomcat:
#Component
public class MultipleHostsTomcatFactory {
#Value("${abc.com.key-store}")
String abcComKeyStore;
#Value("${xyz.com.key-store}")
String xyzComKeyStore;
#Value("${server.port}")
int serverPort;
#Value("${server.http.port:8080}")
private int httpPort;
#Bean
public ServletWebServerFactory servletContainer() throws Exception {
TomcatServletWebServerFactory factor = new TomcatServletWebServerFactory();
factor.addAdditionalTomcatConnectors(createSSLConnectorForMultipleHosts());
return factor;
}
private Connector createSSLConnectorForMultipleHosts() {
Connector connector = null;
try {
connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("https");
connector.setSecure(true);
connector.setProperty("SSLEnabled", "true");
connector.setProperty("defaultSSLHostConfigName", /*"*.abc.com"*/ "*.hostname.com");
connector.setPort(8444);
// *.abc.com
SSLHostConfig sslHostConfig = new SSLHostConfig();
sslHostConfig.setHostName("*.abc.com");
SSLHostConfigCertificate sslHostConfigCertificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.EC);
sslHostConfigCertificate.setCertificateKeystoreFile(abcComKeyStore);
sslHostConfig.addCertificate(sslHostConfigCertificate);
connector.addSslHostConfig(sslHostConfig);
// *.xyz.com
sslHostConfig = new SSLHostConfig();
sslHostConfig.setHostName("*.xyz.com");
sslHostConfigCertificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.EC);
sslHostConfigCertificate.setCertificateKeystoreFile(xyzComKeyStore);
sslHostConfig.addCertificate(sslHostConfigCertificate);
connector.addSslHostConfig(sslHostConfig);
} catch (Exception e) {
System.out.println("Catched exception!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
e.printStackTrace();
}
return connector;
}
}
This code is based on this topic.
But I have an error:
2022-01-13 16:10:21.301 ERROR 22364 --- [ main] org.apache.catalina.util.LifecycleBase : Failed to initialize component [Connector[HTTP/1.1-8444]]
org.apache.catalina.LifecycleException: Protocol handler initialization failed
...
Caused by: java.lang.IllegalArgumentException: Keystore was tampered with, or password was incorrect
...
Caused by: java.io.IOException: Keystore was tampered with, or password was incorrect
...
Caused by: java.security.UnrecoverableKeyException: Password verification failed
Looks like I miss somethimg.
So, how can I force embedded Tomcat to enable SNI?
I solved the problem. Maybe my explanation will help somebody. I just followed the errors that I saw and configured all data that server missed. I don't configure certificate, because I have keystore that stores this certificate. My final server has 2 hosts that support only TLSv1.3 (for my needs):
private Connector createSSLConnectorForMultipleHosts() {
Connector connector = null;
try {
Http2Protocol http2Protocol = new Http2Protocol();
Http11NioProtocol http11Protocol = new Http11NioProtocol();
http2Protocol.setHttp11Protocol(http11Protocol);
connector = new Connector(http11Protocol);
connector.addUpgradeProtocol(http2Protocol);
connector.setScheme("https");
connector.setSecure(true);
connector.setProperty("SSLEnabled", "true");
connector.setProperty("defaultSSLHostConfigName", "my.hostname.com");
connector.setPort(8445);
// first
SSLHostConfig sslHostConfig = new SSLHostConfig();
sslHostConfig.setHostName("my.hostname.com");
sslHostConfig.setCertificateKeystorePassword("mypassword");
sslHostConfig.setCertificateKeystoreFile(firstKeyStore);
sslHostConfig.setProtocols("TLSv1.3");
connector.addSslHostConfig(sslHostConfig);
// second
sslHostConfig = new SSLHostConfig();
sslHostConfig.setHostName("my.another.hostname.com");
sslHostConfig.setCertificateKeystorePassword("mypassword");
sslHostConfig.setCertificateKeystoreFile(secondKeyStore);
sslHostConfig.setProtocols("TLSv1.3");
connector.addSslHostConfig(sslHostConfig);
} catch (Exception e) {
e.printStackTrace();
}
return connector;
}

How to serve static content and resource at same base url with Grizzly

I am using Grizzly to serve my REST service which can have multiple "modules". I'd like to be able to use the same base URL for the service and for static content so I can access all these urls:
http://host:port/index.html
http://host:port/module1/index.html
http://host:port/module1/resource
http://host:port/module2/index.html
http://host:port/module2/resource
The code I'm trying to set this up with looks like this:
private HttpServer createServer(String host, int port, ResourceConfig config)
{
HttpServer server = GrizzlyHttpServerFactory.createHttpServer(URI.create("http://" + host + ":" + port + "/"), config, false);
HttpHandler httpHandler = new CLStaticHttpHandler(HttpServer.class.getClassLoader(), "docs/");
server.getServerConfiguration().addHttpHandler(httpHandler, "/");
return server;
}
With this code, I am only able to see the html pages and I get a "Resource identified by path does not exist" response when I try to get my resources.
When I comment out the code to add the HttpHandler, then I am able to access my resources (but don't have the docs of course).
What do I need to do to access both my resources and my static content?
I ended up writing a service to handle static resources myself. I decided to serve my files from the file system, but this approach would also work for serving them from a jar - you'd just have to get the file as a resource instead of creating the File directly.
#Path("/")
public class StaticService
{
#GET
#Path("/{docPath:.*}.{ext}")
public Response getHtml(#PathParam("docPath") String docPath, #PathParam("ext") String ext, #HeaderParam("accept") String accept)
{
File file = new File(cleanDocPath(docPath) + "." + ext);
return Response.ok(file).build();
}
#GET
#Path("{docPath:.*}")
public Response getFolder(#PathParam("docPath") String docPath)
{
File file = null;
if ("".equals(docPath) || "/".equals(docPath))
{
file = new File("index.html");
}
else
{
file = new File(cleanDocPath(docPath) + "/index.html");
}
return Response.ok(file).build();
}
private String cleanDocPath(String docPath)
{
if (docPath.startsWith("/"))
{
return docPath.substring(1);
}
else
{
return docPath;
}
}
}
One thing you can do is run Grizzly as a servlet container. That way you can run Jersey as servlet filter, and add a default servlet to handle the static content. For example
public class Main {
public static HttpServer createServer() {
WebappContext context = new WebappContext("GrizzlyContext", "");
createJerseyFilter(context);
createDefaultServlet(context);
HttpServer server = GrizzlyHttpServerFactory
.createHttpServer(URI.create("http://localhost:8080/"));
context.deploy(server);
return server;
}
private static void createJerseyFilter(WebappContext context) {
ResourceConfig rc = new ResourceConfig().packages("com.grizzly.test");
// This causes Jersey to forward 404s to default servlet
// which will catch all the static content requests.
rc.property(ServletProperties.FILTER_FORWARD_ON_404, true);
FilterRegistration reg = context.addFilter("JerseyApp", new ServletContainer(rc));
reg.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), "/*");
}
private static void createDefaultServlet(WebappContext context) {
ArraySet<File> baseDir = new ArraySet<>(File.class);
baseDir.add(new File("."));
ServletRegistration defaultServletReg
= context.addServlet("DefaultServlet", new DefaultServlet(baseDir) {});
defaultServletReg.addMapping("/*");
}
public static void main(String[] args) throws IOException {
HttpServer server = createServer();
System.in.read();
server.stop();
}
}
You will need to add the Jersey Grizzly servlet dependency
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-grizzly2-servlet</artifactId>
<version>${jersey2.version}</version>
</dependency>
The only problem with this approach is that the default servlet is meant to serve files from the file system, not from the classpath, as you are currently trying to do. You can see in the createDefaultServlet method I just set the base directory to the current working directory. So that's where all your files would need to be. You can change it to "docs" so all your files would be in the docs folder, which would be in the current working directory.
If you want to read files from the classpath, you may need to implement your own servlet. You can look at the source code for DefaultServlet and try to modify it to serve from the classpath. You can also check out Dropwizard's AssetServlet, which already does serve content from the classpath.
Or you can just say forget it, and just serve from the file system :-)

Tomcat 8 new Resource implementation to map Jar files in a separate directory

With tomcat 8 I have extend the WebAppClassLoader and add some jar filed from a shared location to the classloader path using addRepository() method. With tomcat 8 addRepository have been removed and new resource implementation have been introduced. I'm still able to use the addUrl method to add jar files. But I would like to implement the new resource based implementation.
I've tried with
DirResourceSet dirResourceSet = new DirResourceSet(getContext().getResources(), "/WEB-INF/lib", "/home/thusitha/lib/runtimes/cxf", "/");
WebResourceRoot webResourceRoot = getContext().getResources();
webResourceRoot.getContext().getResources().addPreResources(dirResourceSet);
But this is not working and still it throws classnotfoundexception
Can someone tell me how to map a directory which contains jars to a particular webapp using Tomcat new resource implementation?
A solution to this problem is to register your resources by overriding the ContextConfig class (org.apache.catalina.startup.ContextConfig). Catalina enters a starting state immediately after it scans your document path for resources. Most of the processing of those resources, such as annotations, is handled by the ContextConfig LifecycleListener. To ensure the resources are added before the context configuration takes place, override the ContextConfig.
final Context currentContext = ctx;
ContextConfig ctxCfg = new ContextConfig() {
#Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
WebResourceRoot webResourcesRoot = currentContext.getResources();
String baseDir = Platform.getBaseDir(); // Server Base Directory
File libDir = new File(baseDir + File.separator + "lib");
DirResourceSet dirResourceSet = null;
try {
dirResourceSet = new DirResourceSet(webResourcesRoot, "/WEB-INF/lib", libDir.getCanonicalPath(), "/");
} catch (IOException e) {
throw new RuntimeException(e);
}
webResourcesRoot.addPostResources(dirResourceSet);
String[] possibleJars = dirResourceSet.list("/WEB-INF/lib");
for(String libfile : possibleJars) {
WebResource possibleJar = dirResourceSet.getResource("/WEB-INF/lib/"+libfile);
System.err.println(String.format("Loading possible jar %s",possibleJar.getCanonicalPath())); // Just checking...
if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
WebResourceSet resourceSet = new JarResourceSet(webResourcesRoot, "/WEB-INF/classes", possibleJar.getCanonicalPath(),"/");
webResourcesRoot.addPostResources(resourceSet);
}
}
}
super.lifecycleEvent(event);
}
};
ctx.addLifecycleListener(ctxCfg);
This is an undocumented solution that works on Tomcat 8.0.23. Considering the complexity and difficulty of this I can't say it is a better solution than adding jars directly to ClassLoaders.

Embedded Jetty application

I a newbie in Jetty. I have created an application in which I embed the jetty web container. When I run the the application from eclipse it runs perfectly without any issues. However when I export the project with all the required libraries and run it from command line I cannot access the index.jsp web page like I used to in eclispe. This is the file that run the jetty web container.
public class JettyServer {
// The folder containing all the .jsp files
private final static String WEB_ROOT = "src/WebContent";
// Instance of the Jetty server
private final static Server SRV = new Server();
// Context Path
private final static String CONTEXT_PATH = "/smpp";
// Logging
private final static org.slf4j.Logger logger = LoggerFactory.getLogger(JettyServer.class);
/**
* #param args
* #throws ConfigurationException
*/
public static void main(String[] args) throws ConfigurationException {
logger.info("Initializing Web Server......");
// Servlet Context
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
// Set the security constraints
context.setContextPath(CONTEXT_PATH);
context.setResourceBase(WEB_ROOT);
context.setClassLoader(Thread.currentThread().getContextClassLoader());
context.addServlet(DefaultServlet.class, "/");
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false");
String [] welcomeFiles = {"index.jsp"};
context.setWelcomeFiles(welcomeFiles);
// Set the .jsp servlet handlers
final ServletHolder jsp = context.addServlet(JspServlet.class, "*.jsp");
jsp.setInitParameter("classpath", context.getClassPath());
// Session Manager
SessionHandler sh = new SessionHandler();
context.setSessionHandler(sh);
/* Http Request Handlers */
context.addServlet(HttpRequestProcessor.class, "/HttpHandler");
// Server configuration setup
// Connector setup
// We explicitly use the SocketConnector because the SelectChannelConnector locks files
Connector connector = new SocketConnector();
connector.setHost("localhost");
connector.setPort(Integer.parseInt(System.getProperty("jetty.port", new PropertiesConfiguration("smpp-config.properties").getString("http_port").trim())));
connector.setMaxIdleTime(60000);
JettyServer.SRV.setConnectors(new Connector[] { connector });
JettyServer.SRV.setHandler(context);
JettyServer.SRV.setAttribute("org.mortbay.jetty.Request.maxFormContentSize", 0);
JettyServer.SRV.setGracefulShutdown(5000);
JettyServer.SRV.setStopAtShutdown(true);
logger.info("Starting Jetty Web Container....");
try{
JettyServer.SRV.start();
}
catch(Exception ex){
logger.error("Jetty Web Container failed to start [CAUSE : " + ex.getMessage() + "]");
return;
}
logger.info("Jetty Web Container running....");
while(true){
try{
JettyServer.SRV.join();
}
catch(InterruptedException iex){
logger.error("Jetty Web Container interrupted [CAUSE : " + iex.getMessage() + "]");
}
}
}
}
code formatted properly
Your use of relative paths in the context.setResourceBase("src/WebContent"); will cause you problems.
Use a full, and absolute, URI reference with context.setResourceBase(String).
Note that you can use the following URI schemes: file, ftp, jar, and even http
Instead of this
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
Can you use this ?
WebAppContext root = new WebAppContext();
and rest of the code as example :
String webappDirLocation = "src/Webcontent/";
Server server = new Server(8080);
root.setContextPath(CONTEXT_PATH);
root.setDescriptor(webappDirLocation + "/WEB-INF/web.xml");
root.setResourceBase(webappDirLocation);
root.setParentLoaderPriority(true);
server.setHandler(root);

Embedded jetty ServletTester serving single static file

I'm unit testing with jetty and I want to serve not only my servlet under test but a static page as well. The static page is needed by my application. I'm initializing jetty like this
tester = new ServletTester();
tester.setContextPath("/context");
tester.addServlet(MyServlet.class, "/servlet/*");
tester.start();
What I need now, is something like
tester.addStaticPage("local/path/in/my/workspace", "/as/remote/file");
Is this possible with jetty?
I don't think you can do this with ServletTester. ServletTester creates a single Context for the servlet. You need to set up embedded jetty with at least two contexts: one for the servlet, and one for the static content.
If there was a full WebAppContext, you'd be set, but there isn't.
You could make a copy of ServletTester and add hair, or you can just read up on the API and configure the necessary contexts. Here's a code fragment to show you the basic idea, you will
not be able to compile this as-is. You will need to create a suitable context for the static content.
server = new Server();
int port = Integer.parseInt(portNumber);
if (connector == null) {
connector = createConnector(port);
}
server.addConnector(connector);
for (Webapp webapp : webapps) {
File sourceDirFile = new File(webapp.getWebappSourceDirectory());
WebAppContext wac = new WebAppContext(sourceDirFile.getCanonicalPath(), webapp.getContextPath());
WebAppClassLoader loader = new WebAppClassLoader(wac);
if (webapp.getLibDirectory() != null) {
Resource r = Resource.newResource(webapp.getLibDirectory());
loader.addJars(r);
}
if (webapp.getClasspathEntries() != null) {
for (String dir : webapp.getClasspathEntries()) {
loader.addClassPath(dir);
}
}
wac.setClassLoader(loader);
server.addHandler(wac);
}
server.start();
Set the resource base to the directory containing your static content, and add the jetty "default servlet" to serve that content. I have added the appropriate code to your example below.
tester = new ServletTester();
tester.setContextPath("/context");
tester.setResourceBase("/path/to/your/content");
tester.addServlet(MyServlet.class, "/servlet/*");
tester.addServlet(org.eclipse.jetty.servlet.DefaultServlet.class, "/*");
tester.start();

Categories

Resources