I have exposed an api provided by a jetty server to a front-end application. I want to make sure that only the front-end application (from a certain domain) has access to that api - any other requests should be unauthorised.
What's the best best way of implementing this security feature?
Update: I have set up a CrossOriginFilter - however, I can still access the api via basic GET request from my browser.
Thanks!
Use the IPAccessHandler to setup whitelists and blacklists.
Example: this will allow 127.0.0.* and 192.168.1.* to access everything.
But 192.168.1.132 cannot access /home/* content.
package jetty.demo;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.IPAccessHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
public class IpAccessExample
{
public static void main(String[] args)
{
System.setProperty("org.eclipse.jetty.servlet.LEVEL","DEBUG");
System.setProperty("org.eclipse.jetty.server.handler.LEVEL","DEBUG");
Server server = new Server();
ServerConnector connector = new ServerConnector(server);
connector.setPort(8080);
server.addConnector(connector);
// Setup IPAccessHandler
IPAccessHandler ipaccess = new IPAccessHandler();
ipaccess.addWhite("127.0.0.0-255|/*");
ipaccess.addWhite("192.168.1.1-255|/*");
ipaccess.addBlack("192.168.1.132|/home/*");
server.setHandler(ipaccess);
// Setup the basic application "context" for this application at "/"
// This is also known as the handler tree (in jetty speak)
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/");
// make context a subordinate of ipaccess
ipaccess.setHandler(context);
// The filesystem paths we will map
String homePath = System.getProperty("user.home");
String pwdPath = System.getProperty("user.dir");
// Fist, add special pathspec of "/home/" content mapped to the homePath
ServletHolder holderHome = new ServletHolder("static-home", DefaultServlet.class);
holderHome.setInitParameter("resourceBase",homePath);
holderHome.setInitParameter("dirAllowed","true");
holderHome.setInitParameter("pathInfoOnly","true");
context.addServlet(holderHome,"/home/*");
// Lastly, the default servlet for root content
// It is important that this is last.
ServletHolder holderPwd = new ServletHolder("default", DefaultServlet.class);
holderPwd.setInitParameter("resourceBase",pwdPath);
holderPwd.setInitParameter("dirAllowed","true");
context.addServlet(holderPwd,"/");
try
{
server.start();
server.join();
}
catch (Throwable t)
{
t.printStackTrace(System.err);
}
}
}
Or alternatively, write your own Handler to filter based on some other arbitrary rule.
Such as looking for a required request header, something that your specific front-end application provides, but a browser would not.
package jetty.demo;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.HandlerWrapper;
public class BanBrowserHandler extends HandlerWrapper
{
#Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
String xfe = request.getHeader("X-FrontEnd");
if ((xfe == null) || (!xfe.startsWith("MagicApp-")))
{
// not your front-end
response.sendError(HttpStatus.FORBIDDEN_403);
baseRequest.setHandled(true);
return;
}
getHandler().handle(target,baseRequest,request,response);
}
}
The class IPAccessHandler is deprecated. The InetAccessHandler is recommended.
org.eclipse.jetty.server.Server server = ...;
InetAccessHandler ipaccess = new InetAccessHandler();
ipaccess.include(clientIP);
ipaccess.setHandler(server.getHandler());
server.setHandler(ipaccess);
Related
I have a problem with displaying a webpage in an embedded window but only when creating a standalone jlinked package and only for certain https sites.
I followed the instructions at https://openjfx.io/openjfx-docs/#install-javafx for creating a simple modular App and this works fine when run from the command line with
java --module-path "%PATH_TO_FX%;mods" -m uk.co.comsci.testproj/uk.co.comsci.testproj.Launcher
but after jlinking with the command
jlink --module-path "%PATH_TO_FX_MODS%;mods" --add-modules uk.co.comsci.testproj --output launch
and running with
launch\bin\java.exe -m uk.co.comsci.testproj/uk.co.comsci.testproj.Launcher
the javaFx scene opens but just a blank screen... and I have to use task manager to terminate the App.
If I change the URL to other https sites, it displays fine.
I guess it is down to the security settings and policies somewhere but I have no idea where to start.
I have tried monitoring with WireShark and this shows that when run from java and it works it does some TLSv1.3 stuff to establish the connection. When run as a jlinked package it only does TLSv1.2 stuff. Maybe a clue?
Here's my SSCE:
module-info.java
module uk.co.comsci.testproj {
requires javafx.web;
requires javafx.controls;
requires javafx.media;
requires javafx.graphics;
requires javafx.base;
exports uk.co.comsci.testproj;
}
Launcher.java
package uk.co.comsci.testproj;
public class Launcher {
public static void main(String[] args) {
try {
MainApp.main(args);
} catch (Exception ex) {
System.err.println("Exception!!! " + ex);
}
}
}
MainApp.java
package uk.co.comsci.testproj;
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class MainApp extends Application {
private Stage mainStage;
public static void main(String[] args) throws Exception {
launch(args);
}
#Override
public void start(final Stage initStage) throws Exception {
mainStage = new Stage(StageStyle.DECORATED);
mainStage.setTitle("Test Project");
WebView browser = new WebView();
WebEngine webEngine = browser.getEngine();
// webEngine.load("https://app.comsci.co.uk"); // url);
String uri = "https://test-api.service.hmrc.gov.uk/oauth/authorize"
+ "?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8084%2Fredirect"
+ "&state=lFuLG42uri_aAQ_bDBa9TZGGYD0BDKtFRv8xEaKbeQo"
+ "&client_id=tASN6IpBPt5OcIHlWzkaLXTAyMEa&scope=read%3Avat+write%3Avat";
webEngine.load(uri);
Button closeButt = new Button("Cancel");
closeButt.setOnMouseClicked(event -> {
mainStage.close();
});
HBox closeButBar = new HBox(closeButt);
closeButBar.setAlignment(Pos.BASELINE_RIGHT);
VBox vlo = new VBox(browser, closeButBar);
vlo.setFillWidth(true);
vlo.setSpacing(10.0);
VBox.setVgrow(browser, Priority.ALWAYS);
Scene scene2 = new Scene(vlo, 800, 800);
mainStage.setScene(scene2);
mainStage.initModality(Modality.APPLICATION_MODAL);
mainStage.setTitle("Test connection");
mainStage.showAndWait();
}
}
Any help much appreciated.
OK. Finally tracked it down. So in case anyone has the same problem:
Nothing to do with JavaFx or Webview it was the TLS handshake failing.
Replacing the webview with an http client get
String uri = "https://test-api.service.hmrc.gov.uk/oauth/authorize"
+ "?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8084%2Fredirect"
+ "&state=lFuLG42uri_aAQ_bDBa9TZGGYD0BDKtFRv8xEaKbeQo"
+ "&client_id=tASN6IpBPt5OcIHlWzkaLXTAyMEa&scope=read%3Avat+write%3Avat";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.GET()
.uri(URI.create(uri))
.timeout(Duration.ofSeconds(15))
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("REsponse " + response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
and running with '-Djavax.net.debug=ssl:handshake:verbose' showed that the handshake was failing.
running the embedded keytool -showinfo -tls and comparing this with the system keytool output showed that the TLS_ECDHE_... cyphers where not supported in the jlinked output
A bit of googling and help from here https://www.gubatron.com/blog/2019/04/25/solving-received-fatal-alert-handshake_failure-error-when-performing-https-connections-on-a-custom-made-jre-with-jlink/ showed that all I needed to do was add
requires jdk.crypto.cryptoki;
to my module-info.java :-)
You need to just add the following line in module-info.java
requires jdk.crypto.cryptoki;
I have an Apache Apex application DAG which reads RabbitMQ message from a queue. Which Apache Apex Malhar operator should I use? There are several operators but it's not clear which one to use and how to use it.
Have you looked at https://github.com/apache/apex-malhar/tree/master/contrib/src/main/java/com/datatorrent/contrib/rabbitmq ? There are also tests in https://github.com/apache/apex-malhar/tree/master/contrib/src/test/java/com/datatorrent/contrib/rabbitmq that show how to use the operator
https://github.com/apache/apex-malhar/blob/master/contrib/src/main/java/com/datatorrent/contrib/rabbitmq/AbstractRabbitMQInputOperator.java
That is the main operator code where the tuple type is a generic parameter and emitTuple() is an abstract method that subclasses need to implement.
AbstractSinglePortRabbitMQInputOperator is a simple subclass that provides a single output port and implements emitTuple() using another abstract method getTuple() which needs an implementation in its subclasses.
The tests that Sanjay pointed to show how to use these classes.
I also had problems finding out how to read messages from RabbitMQ to Apache Apex. With the help of the provided links of Sanjay's answer (https://stackoverflow.com/a/42210636/2350644) I finally managed to get it running. Here's how it works all together:
1. Setup a RabbitMQ Server
There are lot of ways installing RabbitMQ that are described here: https://www.rabbitmq.com/download.html
The simplest way for me was using docker (See: https://store.docker.com/images/rabbitmq)
docker pull rabbitmq
docker run -d --hostname my-rabbit --name some-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3-management
To check if RabbitMQ is working, open a browser and navigate to: http://localhost:15672/. You should see the Management page of RabbitMQ.
2. Write a Producer program
To send messages to the queue you can write a simple JAVA program like this:
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.ArrayList;
public class Send {
private final static String EXCHANGE = "myExchange";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE, BuiltinExchangeType.FANOUT);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE, "");
List<String> messages = Arrays.asList("Hello", "World", "!");
for (String msg : messages) {
channel.basicPublish(EXCHANGE, "", null, msg.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + msg + "'");
}
channel.close();
connection.close();
}
}
If you execute the JAVA program you should see some outputs in the Management UI of RabbitMQ.
3. Implement a sample Apex Application
3.1 Bootstrap a sample apex application
Follow the official apex documentation http://docs.datatorrent.com/beginner/
3.2 Add additional dependencies to pom.xml
To use the classes provided by malhar add the following dependencies:
<dependency>
<groupId>org.apache.apex</groupId>
<artifactId>malhar-contrib</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.2.0</version>
</dependency>
3.3 Create a Consumer
We first need to create an InputOperator that consumes messages from RabbitMQ using available code from apex-malhar.
import com.datatorrent.contrib.rabbitmq.AbstractSinglePortRabbitMQInputOperator;
public class MyRabbitMQInputOperator extends AbstractSinglePortRabbitMQInputOperator<String> {
#Override
public String getTuple(byte[] message) {
return new String(message);
}
}
You only have to override the getTuple() method. In this case we simply return the message that was received from RabbitMQ.
3.4 Setup an Apex DAG
To test the application we simply add an InputOperator (MyRabbitMQInputOperator that we implemented before) that consumes data from RabbitMQ and a ConsoleOutputOperator that prints the received messages.
import com.rabbitmq.client.BuiltinExchangeType;
import org.apache.hadoop.conf.Configuration;
import com.datatorrent.api.annotation.ApplicationAnnotation;
import com.datatorrent.api.StreamingApplication;
import com.datatorrent.api.DAG;
import com.datatorrent.api.DAG.Locality;
import com.datatorrent.lib.io.ConsoleOutputOperator;
#ApplicationAnnotation(name="MyFirstApplication")
public class Application implements StreamingApplication
{
private final static String EXCHANGE = "myExchange";
#Override
public void populateDAG(DAG dag, Configuration conf)
{
MyRabbitMQInputOperator consumer = dag.addOperator("Consumer", new MyRabbitMQInputOperator());
consumer.setHost("localhost");
consumer.setExchange(EXCHANGE);
consumer.setExchangeType(BuiltinExchangeType.FANOUT.getType());
ConsoleOutputOperator cons = dag.addOperator("console", new ConsoleOutputOperator());
dag.addStream("myStream", consumer.outputPort, cons.input).setLocality(Locality.CONTAINER_LOCAL);
}
}
3.5 Test the Application
To simply test the created application we can write a UnitTest, so there is no need to setup a Hadoop/YARN cluster.
In the bootstrap application there is already a UnitTest namely ApplicationTest.java that we can use:
import java.io.IOException;
import javax.validation.ConstraintViolationException;
import org.junit.Assert;
import org.apache.hadoop.conf.Configuration;
import org.junit.Test;
import com.datatorrent.api.LocalMode;
/**
* Test the DAG declaration in local mode.
*/
public class ApplicationTest {
#Test
public void testApplication() throws IOException, Exception {
try {
LocalMode lma = LocalMode.newInstance();
Configuration conf = new Configuration(true);
//conf.addResource(this.getClass().getResourceAsStream("/META-INF/properties.xml"));
lma.prepareDAG(new Application(), conf);
LocalMode.Controller lc = lma.getController();
lc.run(10000); // runs for 10 seconds and quits
} catch (ConstraintViolationException e) {
Assert.fail("constraint violations: " + e.getConstraintViolations());
}
}
}
Since we don't need any properties for this application the only thing changed in this file is uncommenting the line:
conf.addResource(this.getClass().getResourceAsStream("/META-INF/properties.xml"));
If you execute the ApplicationTest.java and send messages to RabbitMQ using the Producer program as described in 2., the Test should output all the messages.
You might need to increase the time of the test to see all messages (It is set to 10sec currently).
I'm new to JAX-RS and trying to figure out what is happening here:
I have a simple Hello World Jersey REST service running on Glassfish (Eclipse plugin). I can access it successfully from a browser.
Now, I'd like to call it from a Java class (so I can build JUnit tests around it) but I get this error on buildGet() method:
java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
Unless some magic I'm not aware of happens, I'm not packaging my service and/or client in any jar so it's not related to my application jar signature.
Anyone could explain what I'm doing wrong?
Why is the exception triggered on buildGet() metod and not on any method called before?
My main:
package com.test;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
public class HelloTest {
public static void main(String[] args)
{
Client client = ClientBuilder.newClient();
Response response = null;
try {
WebTarget webTarget = client.target("http://localhost:9595/Hello/api/ping");
Invocation helloInvocation = webTarget.request().buildGet();
response = helloInvocation.invoke();
}
catch (Throwable ex) {
System.out.println(ex.getMessage());
}
finally {
response.close();
}
}
}
My service:
package com.api;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
#Path("ping")
public class Hello
{
#GET
#Produces(MediaType.TEXT_HTML)
public String sayHtmlHello()
{
return "<html>" + "<title>" + "Hello" + "</title>"
+ "<body><h1>" + "Hello!!!" + "</body></h1>" + "</html>";
}
}
After struggling on this for a while, it seems that my Maven configuration had issues and some dependencies were not downloaded/built correctly. I restarted a new project, copied my source files and everything started to work as expected.
We had been using GWT-Dispatch to support the RPC calls using command patterns. We now need to move to GWTP since Dispatch has been absorbed into that project. Would seem to be all well and good. The problem is that we are unable to get a DispatchAsync object anymore. In Dispatch, it was extremely simple to get the default implementation:
private final DispatchAsync dispatchAsync = GWT.create(DefaultDispatchAsync.class);
This no longer works. DefaultDispatchAsync is deprecated, and when we use the suggested replacement for it (RpcDispatchAsync) it looks like this:
private final DispatchAsync dispatchAsync = GWT.create(RpcDispatchAsync.class);
we get the following error:
Rebind result 'com.gwtplatform.dispatch.rpc.client.RpcDispatchAsync' has no default (zero argument) constructors.
Does anyone have an idea about how to do this? I know if we rewrite all the pages to use the GWTP MVP pattern that it's available in the Presenter but moving things over to use full GWTP is a long process and if we can't get the RPC calls up and working quickly that will be a problem for the project.
Thanks in advance -- hopefully it's something easy.
DispatchAsync is no longer generated via deferred binding. Thus you can’t use GWT.create to instantiate it.
GWTP Dispatch is making heavy use of GIN/Guice. So I would recommend that you use this dependency injection framework to get GWTP Dispatch work.
Here is an example, which provides easy access to the DispatchAsync (without the need of rewriting all pages to use the GWTP MVP Pattern):
[Note: This example uses gwtp dispatch 1.0.3]
Client:
MyClientModule.java - configure injection-rules for DispatchAsync
import com.google.gwt.inject.client.AbstractGinModule;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.gwtplatform.dispatch.client.DefaultExceptionHandler;
import com.gwtplatform.dispatch.client.DefaultSecurityCookieAccessor;
import com.gwtplatform.dispatch.client.ExceptionHandler;
import com.gwtplatform.dispatch.client.RpcDispatchAsync;
import com.gwtplatform.dispatch.client.actionhandler.ClientActionHandlerRegistry;
import com.gwtplatform.dispatch.client.actionhandler.DefaultClientActionHandlerRegistry;
import com.gwtplatform.dispatch.shared.DispatchAsync;
import com.gwtplatform.dispatch.shared.SecurityCookie;
import com.gwtplatform.dispatch.shared.SecurityCookieAccessor;
public class MyClientModule extends AbstractGinModule {
private static final String COOKIE_NAME = "JSESSIONID";
#Override
protected void configure() {
bindConstant().annotatedWith(SecurityCookie.class).to(COOKIE_NAME);
bind(ExceptionHandler.class).to(DefaultExceptionHandler.class);
bind(SecurityCookieAccessor.class).to(DefaultSecurityCookieAccessor.class);
bind(ClientActionHandlerRegistry.class).to(DefaultClientActionHandlerRegistry.class);
bind(DispatchAsync.class).toProvider(DispatchAsyncProvider.class).in(Singleton.class);
}
public static class DispatchAsyncProvider implements Provider<DispatchAsync> {
private final DispatchAsync fDispatchAsync;
#Inject
public DispatchAsyncProvider(ExceptionHandler eh, SecurityCookieAccessor sca, ClientActionHandlerRegistry cahr) {
this.fDispatchAsync = new RpcDispatchAsync(eh, sca, cahr);
}
#Override
public DispatchAsync get() {
return fDispatchAsync;
}
}
}
MyClientInjector.java - injector provides access to DispatchAsync
import com.google.gwt.inject.client.GinModules;
import com.google.gwt.inject.client.Ginjector;
import com.gwtplatform.dispatch.shared.DispatchAsync;
#GinModules(MyClientModule.class)
public interface MyClientInjector extends Ginjector {
DispatchAsync getDispatchAsync();
}
Server:
MyGuiceServletContextListener.java - create injector for the servlet, which receives the commands and the servermodule, in which the bindings between (clientside) command and (serverside) handler are defined.
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.servlet.GuiceServletContextListener;
public class MyGuiceServletContextListener extends GuiceServletContextListener {
#Override
protected Injector getInjector() {
return Guice.createInjector(new ServerModule(), new DispatchServletModule());
}
}
DispatchServletModule.java - configures the servlet, which receives the commands
import com.google.inject.servlet.ServletModule;
import com.gwtplatform.dispatch.server.guice.DispatchServiceImpl;
import com.gwtplatform.dispatch.server.guice.HttpSessionSecurityCookieFilter;
import com.gwtplatform.dispatch.shared.Action;
import com.gwtplatform.dispatch.shared.SecurityCookie;
public class DispatchServletModule extends ServletModule {
#Override
public void configureServlets() {
bindConstant().annotatedWith(SecurityCookie.class).to("JSESSIONID");
filter("*").through(HttpSessionSecurityCookieFilter.class);
serve("/" + Action.DEFAULT_SERVICE_NAME + "*").with(DispatchServiceImpl.class);
}
}
ServerModule.java - bindings between (clientside) command and (serverside) handler
import com.gwtplatform.dispatch.server.guice.HandlerModule;
public class ServerModule extends HandlerModule {
#Override
protected void configureHandlers() {
bindHandler(YourCommand.class, YourHandler.class);
}
}
web.xml - tell the web-server to use MyGuiceServletContextListener
<filter>
<filter-name>guiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--
This Guice listener hijacks all further filters and servlets. Extra
filters and servlets have to be configured in your
ServletModule#configureServlets() by calling
serve(String).with(Class<? extends HttpServlet>) and
filter(String).through(Class<? extends Filter)
-->
<listener>
<listener-class>de.gwtpdispatch.server.MyGuiceServletContextListener</listener-class>
</listener>
Usage
Now you can create the injector with deferred binding and get access to the DispatchAsync-instance:
MyClientInjector injector = GWT.create(MyClientInjector.class);
injector.getDispatchAsync().execute(...YourCommand...)
(AND: Don't forget to include the jars of GIN and Guice in your project and add the gin-module to the project's gwt.xml)
I hope this explanation is detailed enough. Happy coding :)
I have a Java Key Store where I store certificates for each of my customer's sub-domain. I am planning to use the server alias to differentiate between multiple customers in the key store as suggested here. Play framework 1.2.7 uses Netty's SslHandler to support SSL on the server-side. I tried implementing a custom SslHttpServerContextFactory that uses this solution.
import play.Play;
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyStore;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Properties;
public class CustomSslHttpServerContextFactory {
private static final String PROTOCOL = "SSL";
private static final SSLContext SERVER_CONTEXT;
static {
String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm");
if (algorithm == null) {
algorithm = "SunX509";
}
SSLContext serverContext = null;
KeyStore ks = null;
try {
final Properties p = Play.configuration;
// Try to load it from the keystore
ks = KeyStore.getInstance(p.getProperty("keystore.algorithm", "JKS"));
// Load the file from the conf
char[] certificatePassword = p.getProperty("keystore.password", "secret").toCharArray();
ks.load(new FileInputStream(Play.getFile(p.getProperty("keystore.file", "conf/certificate.jks"))),
certificatePassword);
// Set up key manager factory to use our key store
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
kmf.init(ks, certificatePassword);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
tmf.init(ks);
final X509KeyManager origKm = (X509KeyManager) kmf.getKeyManagers()[0];
X509KeyManager km = new X509KeyManagerWrapper(origKm);
// Initialize the SSLContext to work with our key managers.
serverContext = SSLContext.getInstance(PROTOCOL);
serverContext.init(new KeyManager[]{km}, tmf.getTrustManagers(), null);
} catch (Exception e) {
throw new Error("Failed to initialize the server-side SSLContext", e);
}
SERVER_CONTEXT = serverContext;
}
public static SSLContext getServerContext() {
return SERVER_CONTEXT;
}
public static class X509KeyManagerWrapper implements X509KeyManager {
final X509KeyManager origKm;
public X509KeyManagerWrapper(X509KeyManager origKm) {
this.origKm = origKm;
}
public String chooseServerAlias(String keyType,
Principal[] issuers, Socket socket) {
InetAddress remoteAddress = socket.getInetAddress();
//TODO: Implement alias selection based on remoteAddress
return origKm.chooseServerAlias(keyType, issuers, socket);
}
#Override
public String chooseClientAlias(String[] keyType,
Principal[] issuers, Socket socket) {
return origKm.chooseClientAlias(keyType, issuers, socket);
}
#Override
public String[] getClientAliases(String s, Principal[] principals) {
return origKm.getClientAliases(s, principals);
}
#Override
public String[] getServerAliases(String s, Principal[] principals) {
return origKm.getServerAliases(s, principals);
}
#Override
public X509Certificate[] getCertificateChain(String s) {
return origKm.getCertificateChain(s);
}
#Override
public PrivateKey getPrivateKey(String s) {
return origKm.getPrivateKey(s);
}
}
}
But, this approach did not work for some reason. I get this message in my SSL debug log.
X509KeyManager passed to SSLContext.init(): need an X509ExtendedKeyManager for SSLEngine use
This is the SSL trace, which fails with "no cipher suites in common". Now, I switched the wrapper to:
public static class X509KeyManagerWrapper extends X509ExtendedKeyManager
With this change, I got rid of the warning, but I still see the same error as before "no cipher suites in common" and here is the SSL trace. I am not sure why the delegation of key manager won't work.
Some more information that may be useful in this context.
Netty uses javax.net.ssl.SSLEngine to support SSL in NIO server.
As per the recommendation in this bug report, it is intentional that X509ExtendedKeyManager must be used with an SSLEngine. So, the wrapper must extend X509ExtendedKeyManager.
This is hindering me to move further with the custom alias selection logic in X509KeyManagerWrapper. Any clues on what might be happening here? Is there any other way to implement this in Netty/Play? Appreciate any suggestions.
SSLEngine uses the chooseEngineServerAlias method to pick the certificate to use (in server mode) - not the chooseServerAlias method.
The default chooseEngineServerAlias implementation actually returns null, which is what causes the "no cipher suites in common" message - you need a certificate to know which cipher suites can be used (e.g. ECDSA can only be used for authentication if the certificate has an ECC public key, etc.) There are actually some cipher suites which can be used without a certificate, however, these are typically disabled as they are vulnerable to MITM attacks.
Therefore, you should also override chooseEngineServerAlias, and implement your logic to select the certificate based on the IP address there. As Netty only uses SSLEngine, what chooseServerAlias does doesn't matter - it'll never be called.
Java 8 also has support for server-side SNI, which allows you to use several certificates across many hostnames with a single IP address. Most web browsers support SNI - the notable exceptions are IE running on Windows XP and some old versions of Android, however, usage of these is declining. I have created a small example application demonstrating how to use SNI in Netty on GitHub. The core part of how it works is by overriding chooseEngineServerAlias - which should give you enough hints, even if you want to use the one certificate per IP address technique instead of SNI.
(I posted a similar answer to this on the Netty mailing list, where you also asked this question - however, my post seems to have not yet been approved, so I thought I'd answer here too so you can get an answer sooner.)