add SslTlsSupport (ported from spray-io), see #3236

- also added TcpPipelineHandler for wrapping a pipeline
- added Java & Scala docs with a complete example
- test verify interop with standard blocking java SSL client and server
- test is placed in akka-remote to benefit from AkkaProvider for
  SecureRandom; should be moved into akka-actor eventually
This commit is contained in:
Roland 2013-04-19 11:19:31 +02:00
parent 3569886bbe
commit 9ba8b115ec
12 changed files with 938 additions and 11 deletions

View file

@ -58,7 +58,7 @@ public class EchoManager extends UntypedActor {
} else if (msg instanceof Tcp.CommandFailed) {
final CommandFailed failed = (CommandFailed) msg;
if (failed.cmd() instanceof Bind) {
log.warning("cannot bind to [{}]", ((Bind) failed.cmd()).endpoint());
log.warning("cannot bind to [{}]", ((Bind) failed.cmd()).localAddress());
getContext().stop(getSelf());
} else {
log.warning("unknown command failed [{}]", failed.cmd());

View file

@ -0,0 +1,218 @@
/**
* Copyright (C) 2013 Typesafe Inc. <http://www.typesafe.com>
*/
package docs.io.japi;
import java.net.InetSocketAddress;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import akka.actor.ActorContext;
import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.actor.UntypedActor;
import akka.event.Logging;
import akka.event.LoggingAdapter;
import akka.io.AbstractPipelineContext;
import akka.io.HasLogging;
import akka.io.SslTlsSupport;
import akka.io.Tcp;
import akka.io.Tcp.Bound;
import akka.io.Tcp.Command;
import akka.io.Tcp.CommandFailed;
import akka.io.Tcp.Connected;
import akka.io.Tcp.Event;
import akka.io.Tcp.Received;
import akka.io.TcpMessage;
import akka.io.TcpPipelineHandler;
import akka.io.TcpPipelineHandler.Init;
import akka.io.ssl.SslTlsSupportSpec;
import akka.testkit.AkkaSpec;
import akka.testkit.JavaTestKit;
import akka.util.ByteString;
public class SslDocTest {
static
//#client
public class SslClient extends UntypedActor {
final InetSocketAddress remote;
final SSLContext sslContext;
final ActorRef listener;
final LoggingAdapter log = Logging
.getLogger(getContext().system(), getSelf());
public SslClient(InetSocketAddress remote, SSLContext sslContext, ActorRef listener) {
this.remote = remote;
this.sslContext = sslContext;
this.listener = listener;
// open a connection to the remote TCP port
Tcp.get(getContext().system()).getManager()
.tell(TcpMessage.connect(remote), getSelf());
}
class Context extends AbstractPipelineContext implements HasLogging {
@Override
public LoggingAdapter getLogger() {
return log;
}
}
Init<HasLogging, Command, Event> init = null;
@Override
public void onReceive(Object msg) {
if (msg instanceof CommandFailed) {
getContext().stop(getSelf());
} else if (msg instanceof Connected) {
// create a javax.net.ssl.SSLEngine for our peer in client mode
final SSLEngine engine = sslContext.createSSLEngine(
remote.getHostName(), remote.getPort());
engine.setUseClientMode(true);
final SslTlsSupport ssl = new SslTlsSupport(engine);
// set up the context for communicating with TcpPipelineHandler
init = new Init<HasLogging, Command, Event>(ssl) {
@Override
public HasLogging makeContext(ActorContext ctx) {
return new Context();
}
};
// create handler for pipeline, setting ourselves as payload recipient
final ActorRef handler = getContext().actorOf(
TcpPipelineHandler.create(init, getSender(), getSelf()));
// register the SSL handler with the connection
getSender().tell(TcpMessage.register(handler), getSelf());
// and send a message across the SSL channel
handler.tell(
init.command(TcpMessage.write(ByteString.fromString("hello"))),
getSelf());
} else if (msg instanceof Init.Event) {
// unwrap TcpPipelineHandlers event into a Tcp.Event
final Event recv = init.event(msg);
if (recv instanceof Received) {
// and inform someone of the received payload
listener.tell(((Received) recv).data().utf8String(), getSelf());
}
}
}
}
//#client
static
//#server
public class SslServer extends UntypedActor {
final SSLContext sslContext;
final ActorRef listener;
final LoggingAdapter log = Logging
.getLogger(getContext().system(), getSelf());
public SslServer(SSLContext sslContext, ActorRef listener) {
this.sslContext = sslContext;
this.listener = listener;
// bind to a socket, registering ourselves as incoming connection handler
Tcp.get(getContext().system()).getManager().tell(
TcpMessage.bind(getSelf(), new InetSocketAddress("localhost", 0), 100),
getSelf());
}
class Context extends AbstractPipelineContext implements HasLogging {
@Override
public LoggingAdapter getLogger() {
return log;
}
}
Init<HasLogging, Command, Event> init = null;
@Override
public void onReceive(Object msg) {
if (msg instanceof CommandFailed) {
getContext().stop(getSelf());
} else if (msg instanceof Bound) {
listener.tell(msg, getSelf());
} else if (msg instanceof Connected) {
// create a javax.net.ssl.SSLEngine for our peer in server mode
final InetSocketAddress remote = ((Connected) msg).remoteAddress();
final SSLEngine engine = sslContext.createSSLEngine(
remote.getHostName(), remote.getPort());
engine.setUseClientMode(false);
final SslTlsSupport ssl = new SslTlsSupport(engine);
// set up the context for communicating with TcpPipelineHandler
init = new Init<HasLogging, Command, Event>(ssl) {
@Override
public HasLogging makeContext(ActorContext ctx) {
return new Context();
}
};
// create handler for pipeline, setting ourselves as payload recipient
final ActorRef handler = getContext().actorOf(
TcpPipelineHandler.create(init, getSender(), getSelf()));
// register the SSL handler with the connection
getSender().tell(TcpMessage.register(handler), getSelf());
} else if (msg instanceof Init.Event) {
// unwrap TcpPipelineHandlers event to get a Tcp.Event
final Event recv = init.event(msg);
if (recv instanceof Received) {
// inform someone of the received message
listener.tell(((Received) recv).data().utf8String(), getSelf());
// and reply (sender is the SSL handler created above)
getSender().tell(init.command(
TcpMessage.write(ByteString.fromString("world"))), getSelf());
}
}
}
}
//#server
private static ActorSystem system;
@BeforeClass
public static void setup() {
system = ActorSystem.create("IODocTest", AkkaSpec.testConf());
}
@AfterClass
public static void teardown() {
system.shutdown();
}
@Test
public void demonstrateSslClient() {
new JavaTestKit(system) {
{
final SSLContext ctx = SslTlsSupportSpec.createSslContext("/keystore", "/truststore", "changeme");
final ActorRef server = system.actorOf(Props.create(SslServer.class, ctx, getRef()));
final Bound bound = expectMsgClass(Bound.class);
assert getLastSender() == server;
final ActorRef client = system.actorOf(Props.create(SslClient.class, bound.localAddress(), ctx, getRef()));
expectMsgEquals("hello");
assert getLastSender() == server;
expectMsgEquals("world");
assert getLastSender() == client;
}
};
}
}

View file

@ -582,6 +582,53 @@ The helper functions are very similar to the ACK-based case:
.. includecode:: code/docs/io/japi/EchoHandler.java#helpers
Usage Example: TcpPipelineHandler and SSL
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This example shows the different parts described above working together. Let us
first look at the SSL server:
.. includecode:: code/docs/io/japi/SslDocTest.java#server
The actor above binds to a local port and registers itself as the handler for
new connections. When a new connection comes in it will create a
:class:`javax.net.ssl.SSLEngine` (details not shown here since they vary wildly
for different setups, please refer to the JDK documentation) and wrap that in
an :class:`SslTlsSupport` pipeline stage (which is included in ``akka-actor``).
This single-stage pipeline will be driven by a :class:`TcpPipelineHandler`
actor which is also included in ``akka-actor``. In order to capture the generic
command and event types consumed and emitted by that actor we need to create a
wrapper—the nested :class:`Init` class—which also provides the
:meth:`makeContext` method for creating the pipeline context needed by the
supplied pipeline. With those things bundled up all that remains is creating a
:class:`TcpPipelineHandler` and registering that one as the recipient of
inbound traffic from the TCP connection.
Since we instructed that handler actor to send any events which are emitted by
the SSL pipeline to ourselves, we can then just wait for the reception of the
decrypted payload messages, compute a response—just ``"world"`` in this
case—and reply by sending back a ``Tcp.Write``. It should be noted that
communication with the handler wraps commands and events in the inner types of
the ``init`` object in order to keep things well separated. To ease handling of
such path-dependent types there exist two helper methods, namely
:class:`Init.command` for creating a command and :class:`Init.event` for
unwrapping an event.
.. warning::
The :class:`TcpPipelineHandler` does currently not handle back-pressure from
the TCP socket, i.e. it will just lose data when the kernel buffer
overflows. This will be fixed before Akka 2.2 final.
Looking at the client side we see that not much needs to be changed:
.. includecode:: code/docs/io/japi/SslDocTest.java#client
Once the connection is established we again create a
:class:`TcpPipelineHandler` wrapping an :class:`SslTlsSupport` (in client mode)
and register that as the recipient of inbound traffic and ourselves as
recipient for the decrypted payload data. The we send a greeting to the server
and forward any replies to some ``listener`` actor.
Using UDP
---------

View file

@ -601,6 +601,40 @@ The helper functions are very similar to the ACK-based case:
.. includecode:: code/docs/io/EchoServer.scala#helpers
Usage Example: TcpPipelineHandler and SSL
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This example shows the different parts described above working together:
.. includecode:: ../../../akka-remote/src/test/scala/akka/io/ssl/SslTlsSupportSpec.scala#server
The actor above is meant to be registered as the inbound connection handler for
a listen socket. When a new connection comes in it will create a
:class:`javax.net.ssl.SSLEngine` (details not shown here since they vary wildly
for different setups, please refer to the JDK documentation) and wrap that in
an :class:`SslTlsSupport` pipeline stage (which is included in ``akka-actor``).
This single-stage pipeline will be driven by a :class:`TcpPipelineHandler`
actor which is also included in ``akka-actor``. In order to capture the generic
command and event types consumed and emitted by that actor we need to create a
wrapper—the nested :class:`Init` class—which also provides the
:meth:`makeContext` method for creating the pipeline context needed by the
supplied pipeline. With those things bundled up all that remains is creating a
:class:`TcpPipelineHandler` and registering that one as the recipient of
inbound traffic from the TCP connection.
Since we instructed that handler actor to send any events which are emitted by
the SSL pipeline to ourselves, we can then just switch behavior to receive the
decrypted payload message, compute a response and reply by sending back a
``Tcp.Write``. It should be noted that communication with the handler wraps
commands and events in the inner types of the ``init`` object in order to keep
things well separated.
.. warning::
The :class:`TcpPipelineHandler` does currently not handle back-pressure from
the TCP socket, i.e. it will just lose data when the kernel buffer
overflows. This will be fixed before Akka 2.2 final.
Using UDP
---------