Create a DSL in Kotlin

Kotlin as a programming language provides some very powerful features, which allow the creation of custom internal Domain Specific Languages (DSL).  One of these features, I also wrote about on this blog, is called Function Literals with Receiver, others are the invoke convention or infix notation. In this article, I will show how to create a Kotlin DSL by introducing a library that exposes a DSL as its API. I've often been struggling with Java’s API when I had to set up SSL/TLS connections in scenarios where I e.g. needed to implement HTTPS communication. I always felt like wanting to write a little library that can support me with this task, hiding away all the difficulties and of course the boilerplate needed for it.

Domain Specific Language

The term Domain Specific Language is used very broadly nowadays but in the case of what I’ll be talking about it’s referring to some kind of "mini-language". It's used to describe the construction of a specific domain object in a semi-declarative way. Examples of DSLs are Groovy builders for creating XML, HTML or UI data. The best example, in my opinion though, is the build tool Gradle itself, which also uses Groovy-based DSLs for describing software build automation [1].
To put their purpose simply, DSLs are a way of providing an API that is cleaner, more readable and, most importantly, more structured than traditional APIs. Instead of having to call individual functions in an imperative way, DSLs use nested descriptions, which creates clean structures; we could even call it "grammar". DSLs define possibilities of combining different constructs with each other and also make copious use of scopes, in which different functionalities can be used.

Why Kotlin is particularly good for DSLs

As we already know, Kotlin is a statically typed language, which enables features that aren’t available in dynamically typed languages like Groovy *duck away*. Most importantly, static typing allows error detection at compile time and much better IDE support in general.

Okay, let’s not waste time with theory and start having fun with DSLs, meaning a lot of nested lambdas! So, you better know how to use lambdas in Kotlin 🙂

Kotlin DSL by example

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.

Java Secure Socket Extension

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:

jsse_classes

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.

Setting up a TLS connection in Java

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.

public class TLSConfiguration { ... }
public class StoreType { ... }

 public void connectSSL(String host, int port,
        TLSConfiguration tlsConfiguration) throws IOException {

        String tlsVersion = tlsConfiguration.getProtocol();
        StoreType keystore = tlsConfiguration.getKeystore();
        StoreType trustStore = tlsConfiguration.getTruststore();
        try {
            SSLContext ctx = SSLContext.getInstance(tlsVersion);
            TrustManager[] tm = null;
            KeyManager[] km = null;
            if (trustStore != null) {
                tm = getTrustManagers(trustStore.getFilename(), 
                        trustStore.getPassword().toCharArray(),
                        trustStore.getStoretype(), trustStore.getAlgorithm());
            }
            if (keystore != null) {
                km = createKeyManagers(keystore.getFilename(), 
                        keystore.getPassword(),
                        keystore.getStoretype(), keystore.getAlgorithm());
            }
            ctx.init(km, tm, new SecureRandom());
            SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();
            SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
                                  host, port);
            sslSocket.startHandshake();
        } catch (Exception e) {
            throw new IllegalStateException("Not working :-(", e);
        }
    }


    private static TrustManager[] getTrustManagers(
        final String path, final char[] password,
        final String storeType, final String algorithm) throws Exception {

        TrustManagerFactory fac = TrustManagerFactory.getInstance(
               algorithm == null ? "SunX509" : algorithm);
        KeyStore ks = KeyStore.getInstance(
               storeType == null ? "JKS" : storeType);
        Path storeFile = Paths.get(path);
        ks.load(new FileInputStream(storeFile.toFile()), password);
        fac.init(ks);
        return fac.getTrustManagers();
    }

    private static KeyManager[] createKeyManagers(
        final String filename, final String password,
        final String keyStoreType, final String algorithm) throws Exception {

        KeyStore ks = KeyStore.getInstance(
                keyStoreType == null ? "PKCS12" : keyStoreType);
        ks.load(new FileInputStream(filename), password.toCharArray());
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(
                algorithm == null ? "SunX509" : algorithm);
        kmf.init(ks, password.toCharArray());
        return kmf.getKeyManagers();
    }

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:

Set up TLS connection with Kotlin

 fun connectSSL(host: String, port: Int, protocols: List<String>, kmConfig: Store?, tmConfig: Store?){
    val context = createSSLContext(protocols, kmConfig, tmConfig)
    val sslSocket = context.socketFactory.createSocket(host, port) as SSLSocket
    sslSocket.startHandshake()
}

fun createSSLContext(protocols: List<String>, kmConfig: Store?, tmConfig: Store?): SSLContext {
    if (protocols.isEmpty()) {
        throw IllegalArgumentException("At least one protocol must be provided.")
    }
    return SSLContext.getInstance(protocols[0]).apply {
        val keyManagerFactory = kmConfig?.let { conf ->
            val defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm()
            KeyManagerFactory.getInstance(conf.algorithm ?: defaultAlgorithm).apply {
                init(loadKeyStore(conf), conf.password)
            }
        }
        val trustManagerFactory = tmConfig?.let { conf ->
            val defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
            TrustManagerFactory.getInstance(conf.algorithm ?: defaultAlgorithm).apply {
                init(loadKeyStore(conf))
            }
        }

        init(keyManagerFactory?.keyManagers, trustManagerFactory?.trustManagers,
            SecureRandom())
    }
}

fun loadKeyStore(store: Store) = KeyStore.getInstance(store.fileType).apply {
    load(FileInputStream(store.name), store.password)
}

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.

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.

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 ofStore 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 theTLSSocketFactoryProvider, 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:

 val fac = socketFactory {
        trustManager {
            if (System.currentTimeMillis() % 2 == 0L) {
                open("any") withPass "123456" beingA "jks"
            } else {
                open("other") withPass "123456" beingA "jks"
            }
        }
    }

Library on GitHub

The code we just looked at is available on GitHub. If you have any ideas or concerns, just tell me.

For those of you being experts in TLS and the JSSE lib in particular: I’m aware, that the library does not contain many cases and possibilities of JSSE yet. I hope to find people interested in this kind of library so that we can find ways to extend it accordingly.

Wrap-up

We’ve seen why DSLs can be a better way for providing APIs to a client by the example of setting up a TLS connection using the JSSE library. Kotlin is a really great language for writing such APIs because of its static typing and great features. Many other Kotlin DSL examples are available and you can find them on GitHub. As a starter, have a look at Kotlintest or kotlinx.html for instance.

If you like to have a look at my examples or even want to contribute, the code is available here: SeKurity. Feel free to give any feedback, I’m always happy to help.

If you want to read more about Kotlin's beautiful features I recommend the book Kotlin in Action to you and also like to direct you to my other articles 🙂


1. There’s also a Kotlin DSL for Gradle available: Gradle-Script-Kotlin

 

11 thoughts on “Create a DSL in Kotlin

Leave a Reply

Your email address will not be published. Required fields are marked *