As already stated in the introductory part of this post, we’ll use Java’s API for setting up SSL/TLS connections as an example here. If you’re not familiar with it, the following will provide a short introduction.

We can see quite a few classes, which need to be combined in some meaningful way. You often start at the very beginning by creating a trust store and a key store and use in combination with a random generator for setting up the SSLContext . The SSLContext is used for creating a SSLSocketFactory or SSLServerSocketFactory , which then provides the Socket instances. This sounds pretty easy but let's observe how it looks when expressed in Java code.

Java Secure Socket Extension (JSSE) is a library that’s part of Java SE since 1.4. It provides functionalities for creating secure connections via SSL/TLS, including client/server authentication, data encryption, and message integrity. Like many others, I find security topics rather tricky despite the fact that I’m using the features quite often in my day-to-day work. One reason for this is probably the great number of possible API combinations, another its verbosity needed to set up such connections. Have a look at the class hierarchy:

Okay... it’s Java, right? Of course, it’s verbose with a lot of handled checked exceptions and resources that need to be managed, which I simplified already for the sake of brevity here. As a next step, let’s see how this looks converted into Kotlin code:

The abstractly described task of assembling the bits and pieces together took me little more than 100 lines of code. The following snippet shows a function that can be used to connect to a TLS server with optional mutual authentication, which is needed if both parties, client and server, need to trust each other. The classes can be found in the javax.net.ssl package.

Set up TLS connection with Kotlin

We’ve seen by now that Kotlin can be a lot more concise than Java but that's yesterday's news. In the next section, we will finally see how that code can be wrapped in a DSL, which is then being exposed as an API to the client.

You might notice that the shown code is not a one-to-one conversion, which is because Kotlin provides a set of very useful functions in its standard library that often help with writing smarter code. This small piece of source code contains four usages of apply , a method that makes use of Function Literals with Receiver . It's one of Kotlin's famous scope functions , which creates a scope on an arbitrary context object, in which we access members of that context object without additional qualifiers.

Create a DSL in Kotlin

The first thing to think about when creating an API (and this certainly also applies to DSLs), is how we can make it easy for the client. We need to define certain configuration parameters that need to be provided by the user.

In our case, this is quite simple. We need zero or one description of a keystore and a truststore respectively. Also, it’s important that accepted cipher suites and the socket connection timeouts are known. Last but not least, it’s mandatory to provide a set of protocols for our connection, which would be something like TLSv1.2 for example. For every configuration, value defaults are made available and will be used if needed.

The described values can easily be wrapped into a configuration class, which we’ll call ProviderConfiguration since it will be used for configuring a TLSSocketFactoryProvider later on.

The Configuration

class ProviderConfiguration { var kmConfig: Store? = null var tmConfig: Store? = null var socketConfig: SocketConfiguration? = null fun open(name: String) = Store(name) fun sockets(configInit: SocketConfiguration.() -> Unit) { this.socketConfig = SocketConfiguration().apply(configInit) } fun keyManager(store: () -> Store) { this.kmConfig = store() } fun trustManager(store: () -> Store) { this.tmConfig = store() } }

We can see three nullable properties here, each of which is null by default because the clients might not want to configure everything for their connection. The relevant methods in this class are sockets() , keyManager() and trustManager() , all of which have a single parameter of a function type. The sockets() method goes even a step further by defining a function literal with receiver, which is SocketConfiguration here. This enables the client to pass in a lambda that has access to all members of SocketConfiguration as we know it from extension functions and shown with apply in earlier examples.

The socket() method provides the receiver by creating a new instance and then invoking the passed function on it with apply . The resulting configured instance is then used as a value for the internal property. Both other functions are a bit easier as they define simple functions types, without a receiver, as their parameters. They simply expect a provider of an instance of Store , which then is set on the internal property.

Store and SocketConfiguration Here you can observce the classes Store and SocketConfiguration :

data class SocketConfiguration( var cipherSuites: List<String>? = null, var timeout: Int? = null, var clientAuth: Boolean = false) class Store(val name: String) { var algorithm: String? = null var password: CharArray? = null var fileType: String = "JKS" infix fun withPass(pass: String) = apply { password = pass.toCharArray() } infix fun beingA(type: String) = apply { fileType = type } infix fun using(algo: String) = apply { algorithm = algo } }

The first one is as easy as it could get, a simple data class with, once again, nullable properties. Store is a bit unique though as it, in addition to its properties, defines three infix functions, which are acting as setters for the properties basically. We again make use of apply here because it returns its context object after invocation and is used as a tool for providing a fluent API here; the methods can be chained later on. One thing I haven’t mentioned so far is the open(name: String) function defined in ProviderConfiguration . This one is supposed to be used as a factory for instances of Store and we are about to see this in action soon. All of this in combination creates a neat way for defining the necessary configuration data. Before we can have a look at the client side, it's necessary to observe the TLSSocketFactoryProvider , which has to be configured with the classes I just introduced. The Kotlin DSL Core

class TLSSocketFactoryProvider(init: ProviderConfiguration.() -> Unit) { private val config: ProviderConfiguration = ProviderConfiguration().apply(init) fun createSocketFactory(protocols: List): SSLSocketFactory = with(createSSLContext(protocols)) { return ExtendedSSLSocketFactory( socketFactory, protocols.toTypedArray(), getOptionalCipherSuites() ?: socketFactory.defaultCipherSuites ) } fun createServerSocketFactory(protocols: List): SSLServerSocketFactory = with(createSSLContext(protocols)) { return ExtendedSSLServerSocketFactory( serverSocketFactory, protocols.toTypedArray(), getOptionalCipherSuites() ?: serverSocketFactory.defaultCipherSuites ) } private fun getOptionalCipherSuites() = config.socketConfig?.cipherSuites?.toTypedArray() private fun createSSLContext(protocols: List<String>): SSLContext { //... already shown earlier } }

This one isn’t hard to understand either. Most of the DSL's content has already been shown in createSSLContext() earlier.

The most important thing in this listing is the constructor. It expects a function with a ProviderConfiguration as a receiver. Internally it creates a new instance of it and calls this function in order to initialize the configuration. The configuration is used in TLSSocketFactoryProvider 's other functions for setting up a SocketFactory as soon as one of the public methods createSocketFactory or createServerSocketFactory is being called. Client API and Usage of DSL

val defaultTLSProtocols = listOf("TLSv1.2") fun serverSocketFactory( protocols: List<String> = defaultTLSProtocols, configuration: ProviderConfiguration.() -> Unit = {}) = with(TLSSocketFactoryProvider(configuration)) { this.createServerSocketFactory(protocols) } fun socketFactory( protocols: List<String> = defaultTLSProtocols, configuration: ProviderConfiguration.() -> Unit = {}) = with(TLSSocketFactoryProvider(configuration)) { this.createSocketFactory(protocols) }

In order to assemble all of this DSL together, simple top-level functions were created, which represent the client’s entry point to this DSL. These two functions only delegate a function literal with ProviderConfiguration receiver to a created instance of TLSSocketFactoryProvider , which is used to create corresponding socket and server socket factories via createSocketFactory and createServerSocketFactory respectively.

Finally, we can easily use this DSL and create some sockets with it:

val fac = socketFactory { keyManager { open("certsandstores/clientkeystore") withPass "123456" beingA "jks" } trustManager { open("certsandstores/myTruststore") withPass "123456" beingA "jks" } sockets { cipherSuites = listOf("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA") timeout = 10_000 } } val socket = fac.createSocket("192.168.3.200", 9443)

Let’s recap: The top-level function socketFactory expects a lambda, which has access to ProviderConfiguration members since it’s the lambda’s receiver. Therefore we can call keyManager() , trustManager() and sockets() without any additional prefix here. The functions keyManager() and trustManager() take an instance of Store , which we create by calling ProviderConfiguration::open and Store 's infix functions. The sockets() method is different as it expects a function literal with SocketConfiguration receiver, which is a data class and therefore provides access to its properties directly.

I hope this is understandable. It’s absolutely inevitable to fully understand how lambdas work in Kotlin, the ones with receivers in particular.

In my humble opinion, this is a very clear definition of a SocketFactory and much easier to understand than the standard Java way shown earlier. Another feature provided by a DSL like this is the possibility to make use of all language features and other methods that are available in the receiver’s contexts. You could easily read values from a file for creating the store configurations or use loops, if and when constructs etc. whenever you need to: