8. chat echo (clear-then-TLS)

import std.socket : InternetAddress, Socket, TcpSocket, SocketOptionLevel, SocketOption;
import std.exception : enforce, ifThrown;
import std.stdio : writefln, writeln, stderr;
import std.string : toStringz, fromStringz, representation;
import std.format : format;
import deimos.openssl.ssl;
import deimos.openssl.err : ERR_print_errors_fp;
static import cio = core.stdc.stdio;
import hostname : hostnamez;
import sslclient : SSLClient;

void main() {
    ssl_ctx_st* ctx = SSL_CTX_new(TLS_server_method);
    enforce(ctx, "SSL_CTX_new() failed");
    scope (exit)
        SSL_CTX_free(ctx);

    if (!SSL_CTX_use_certificate_file(ctx, "ssl.crt", SSL_FILETYPE_PEM)
            || !SSL_CTX_use_PrivateKey_file(ctx, "ssl.key", SSL_FILETYPE_PEM)) {
        ERR_print_errors_fp(cio.stderr);
        enforce(false, "SSL_CTX_use_certificate_file() failed");
    }

    enum port = 4444;
    scope listener = new TcpSocket;
    listener.blocking = true;
    listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, 1);
    listener.bind(new InternetAddress(port));
    listener.listen(10);
    writefln!"Listening on %d."(port);
    ubyte[1024] buf;
    ptrdiff_t len;

    while (true) {
        writeln;
        auto client = SSLClient(listener.accept);
        writefln!"Received connection from %s."(client.sock.remoteAddress.toString);

        while (0 < (len = client.recv(buf[]))) {
            if (buf[0 .. len] == "STARTTLS\n") {
                if (!client.accept(ctx).ifThrown(0)) {
                    writeln("Failed to secure socket.");
                    ERR_print_errors_fp(cio.stdout);
                    break;
                }
                client.send(format!"SSL/TLS using %s\n"(SSL_get_cipher(client.ssl)
                        .fromStringz).representation);
            } else {
                client.send(buf[0 .. len]);
            }
        }
    }
    assert(0);
}

sslclient.d

import std.socket : Socket;
import std.exception : enforce;
import std.string : toStringz, fromStringz, representation;
import deimos.openssl.ssl : ssl_st, ssl_ctx_st, SSL_new, SSL_set_tlsext_host_name,
    SSL_set_fd, SSL_accept, SSL_connect, SSL_read, SSL_write, SSL_shutdown, SSL_free;
import hostname : hostnamez;

struct SSLClient {
    Socket sock;
    ssl_st* ssl;

    int recv(ubyte[] buf) @trusted {
        assert(buf.ptr !is null);
        assert(buf.length < int.max);
        if (ssl)
            return SSL_read(ssl, cast(void*) buf.ptr, cast(int) buf.length);
        else
            return cast(int) sock.receive(buf);
    }

    int send(const ubyte[] buf) @trusted {
        assert(buf.ptr !is null);
        assert(buf.length < int.max);
        if (ssl)
            return SSL_write(ssl, cast(void*) buf.ptr, cast(int) buf.length);
        else
            return cast(int) sock.send(buf);
    }

    private void ssl_prelude(ssl_ctx_st* ctx) {
        assert(ssl is null);
        enforce(ssl = SSL_new(ctx), "SSL_new() failed");
        enforce(SSL_set_tlsext_host_name(ssl, cast(char*) hostnamez),
                "SSL_set_tlsext_host_name failed");
        enforce(SSL_set_fd(ssl, sock.handle), "SSL_set_fd() failed");
    }

    bool accept(ssl_ctx_st* ctx) {
        ssl_prelude(ctx);
        enforce(SSL_accept(ssl) == 1, "SSL_accept() failed");
        return true;
    }

    bool connect(ssl_ctx_st* ctx) {
        ssl_prelude(ctx);
        enforce(SSL_connect(ssl) == 1, "SSL_connect() failed");
        return true;
    }

    ~this() {
        if (ssl) {
            SSL_shutdown(ssl);
            SSL_free(ssl);
        }
        sock.close;
    }
}

This server is the first to require a proper dub setup, with multiple configurations to reuse an sslclient.d library that wraps std.socket.Socket with an SSL state, which has I/O functions that either use raw socket I/O or SSL I/O depending on the SSL state. You should check out the dub directory with fossil:

$ fossil clone https://d.minimaltype.com/
(a bunch of fossil output)
$ cd d/net/8
$ make ssl.pem
(a bunch of openssl output)
$ dub build --config=server
Performing "debug" build using /usr/bin/dmd for x86_64.
net8 ~master: building configuration "server"...
Linking...
$ dub build --config=client
Performing "debug" build using /usr/bin/dmd for x86_64.
net8 ~master: building configuration "client"...
Linking...

Server interaction

Listening on 4444.

Received connection from 127.0.0.1:57268

Client interaction

this is cleartext
this is cleartext
STARTTLS
subject: /C=AU/ST=Some-State/O=Internet Widgits Pty Ltd
issuer: /C=AU/ST=Some-State/O=Internet Widgits Pty Ltd
Accepting self-signed certificate.
SSL/TLS using TLS_AES_256_GCM_SHA384
^^ three lines from the client, then the last line from the server
^^ three lines from the client, then the last line from the server
this is ciphertext
this is ciphertext
hi
hi
^C
The 'STARTTLS' line is input to the client that causes it and the server to negotiate an encrypted connection.

strace of the client's "hi" exchange

strace: Process 178463 attached
read(0, "hi\n", 1024)                   = 3
write(3, "\27\3\3\0\24\377x\325/\315\31f\3615\240!\23\230cv\365\240\232\3161", 25) = 25
read(3, "\27\3\3\0\24", 5)              = 5
read(3, "^\261U\v\247\354\24\231\327~\302\210\\\322u\251-\300\337=", 20) = 20
write(1, "hi\n", 3)                     = 3
read(0, ^Cstrace: Process 178463 detached
showing cleartext to the terminal, and encrypted communications over the socket.