SSL/TLS support added (original) (raw)

Some time last year, Elie Chaftari implemented an OpenSSL binding in Factor (and by the way, Elie, good luck with Medioset!). I took this binding and extended it into a high-level SSL library which integrates with Factor's streams and sockets. The effort involved was non-trivial, because OpenSSL's nonblocking I/O behavior is somewhat confusing and not very well documented. So far it only works on Unix. Integration with the Windows overlapped I/O system is still pending.
I will start by presenting a simple client/server application that uses standard sockets, then convert it to use SSL.
We will implement an application which sends the current time of day, formatted as an RFC822 string, to the user.
First of all, how do we print such a string? We get the current time with now, which pushes a timestamp object on the stack, then convert it to an RFC822 string with timestamp>rfc822:

( scratchpad ) USING: calendar calendar.format ;
( scratchpad ) now timestamp>rfc822 print
Thu, 22 May 2008 01:25:18 -0500

We want to send this to every client that connects, so we use the with-server combinator from the io.server vocabulary. It takes four parameters:

Here is a daytime-server word, with the complete with-server form:

USING: calendar.format calendar io.server io.encodings.ascii ;

: daytime-server ( -- )
9670 internet-server
"daytime"
ascii
[ now timestamp>rfc822 print ]
with-server ;

We can start our server as follows:

USE: threads

[ daytime-server ] in-thread

Now, let's test it with telnet:

slava$ telnet localhost 9670
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Thu, 22 May 2008 01:32:17 -0500
Connection closed by foreign host.

Works fine. The log file logs/daytime/1.log will have now logged something like the following:

[2008-05-22T01:32:17-05:00] NOTICE accepted-connection: { T{ inet6 f "0:0:0:0:0:0:0:1" 50916 } }

Now, let's write a client for this service. We'll write the client using the with-client combinator from io.sockets. It takes three parameters:

Here is the complete with-client form:

USING: io io.sockets io.encodings.ascii ;

"localhost" 9670 ascii [ readln ] with-client

We're not quite done though; we should really parse the timestamp from its RFC822 string representation back into a timestamp object. We can do this with the rfc822>timestamp word. Here is the complete daytime-client word, refactored to take a host name from the stack:

USING: calendar.format io io.sockets io.encodings.ascii ;

: daytime-client ( hostname -- timestamp )
9670 ascii [ readln ] with-client
rfc822>timestamp ;

Let's test our program, using the describe word to get a verbose description of the timestamp returned:

( scratchpad ) "localhost" daytime-client describe
timestamp instance
"delegate" f
"year" 2008
"month" 5
"day" 22
"hour" 1
"minute" 37
"second" 47
"gmt-offset" T{ duration f 0 0 0 -5 0 0 }

So we're done! Here is the complete program:

USING: calendar.format calendar
io io.server io.sockets
io.encodings.ascii ;
IN: daytime

: daytime-server ( -- )
9670 internet-server
"daytime"
ascii
[ now timestamp>rfc822 print ]
with-server ;

: start-daytime-server ( -- )
[ daytime-server ] in-thread ;

: daytime-client ( hostname -- timestamp )
9670 ascii [ readln ] with-client
rfc822>timestamp ;

You can put this in a file named work/daytime/daytime.factor in the Factor directory, then enter the following in the listener:

( scratchpad ) USE: daytime
Loading P" resource:work/daytime/daytime.factor"
( scratchpad ) start-daytime-server
( scratchpad ) "localhost" daytime-client describe

So we have a simple client/server application with support for multiple connections and logging.

Let's add SSL support! There are four things to change.

The secure-server word is much like internet-server in that it takes a port number and outputs a list of addresses, except this time the addresses are secure addresses, which means that a server socket listening on one will accept SSL connections:
( scratchpad ) 9671 secure-server .
{
T{ secure f T{ inet6 f "0:0:0:0:0:0:0:0" 9671 } }
T{ secure f T{ inet4 f "0.0.0.0" 9671 } }
}

There are several ways to go about obtaining a server certificate. You can either fork out some money to a CA such as VeriSign or GoDaddy (prices range from 15to15 to 15to3000), or you can generate a self-signed certificate, for example by following these directions. Once you have the two files, dh1024.pem and server.pem, place them inside work/daytime/, and modify start-daytime-server to wrap the daytime-server call with the with-secure-context combinator. This combinator takes a secure-config object from the stack, and we fill in the slots of this object with the pathnames of the two files we just generated:
: start-daytime-server ( -- )
[

"resource:work/daytime/dh1024.pem" >>dh-file
"resource:work/daytime/server.pem" >>key-file
[ daytime-server ] with-secure-context
] in-thread ;

Note that you must nest with-secure-context inside in-thread, not vice versa, because in-thread returns immediately, and with-secure-context destroys the context after its quotation returns.

However, by default, the client will validate the server's certificate, and unless you're using a certificate signed by a root CA, this will fail.
To disable verification, we can wrap client calls in an SSL configuration with verification disabled:
f >>verify [ "localhost" daytime-client ] with-secure-context

Here is our complete daytime client/server vocabulary which uses SSL:

USING: calendar.format calendar
io io.server io.sockets io.sockets.secure
io.encodings.ascii ;
IN: daytime

: daytime-server ( -- )
9671 secure-server
"daytime"
ascii
[ now timestamp>rfc822 print ]
with-server ;

: start-daytime-server ( -- )
[

"resource:work/daytime/dh1024.pem" >>dh-file
"resource:work/daytime/server.pem" >>key-file
[ daytime-server ] with-secure-context
] in-thread ;

: daytime-client ( hostname -- timestamp )
9671 ascii [ readln ] with-client
rfc822>timestamp ;

: daytime-client-no-verify ( hostname -- timestamp )
#! Use this if your server has a self-signed certificate
f >>verify
[ daytime-client ] with-secure-context ;

That's it.
There is one more topic to cover, and that is mixed secure/insecure servers. Often you want to listen for insecure connections on one port, and secure connections on another. We can achieve this easily enough by concatenating the results of internet-server and secure-server:

( scratchpad ) 9670 internet-server 9671 secure-server append .
{
T{ inet6 f "0:0:0:0:0:0:0:0" 9670 }
T{ inet4 f "0.0.0.0" 9670 }
T{ secure f T{ inet6 f "0:0:0:0:0:0:0:0" 9671 } }
T{ secure f T{ inet4 f "0.0.0.0" 9671 } }
}

The server will now listen on all four addresses, and the quotation can differentiate between insecure and secure clients by checking if the value of the remote-address variable is an instance of secure or not using the secure? word.
So what is missing? Several things: