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:- A sequence of addresses to listen on. We create these addresses by passing a port number to the
internet-server
word. It gives us back a sequence of addresses:( scratchpad ) 9670 internet-server .
{
T{ inet6 f "0:0:0:0:0:0:0:0" 9670 }
T{ inet4 f "0.0.0.0" 9670 }
} - A service name for logging purposes. We pass
"daytime"
, which means that connections will be logged to thelogs/daytime/1.log
file in the Factor directory. We can then invokerotate-logs
to move1.log
to2.log
, and so on. Log analysis tools are available too. I've discussed this in a prior blog post. - Finally, a quotation which is run for each client connection, in a new thread. Our quotation just prints the current time:
[ now timestamp>rfc822 print ]
- An I/O encoding specifier for client connections. We only care about ASCII so we pass
ascii
.
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:- An address specifier. This is the server to connect to. We're going to connect to port 9670 on
localhost
, so we pass"localhost" 9670 <inet>
. - An encoding specifier. Again, we're only interested in 7-bit ASCII, so we pass
ascii
. - The final parameter is a quotation. We wish to a line of input from the server and leave it on the stack, so we pass
[ readln ]
.
Here is the complete
with-client
form:USING: io io.sockets io.encodings.ascii ;
"localhost" 9670 <inet> 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 <inet> 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 <inet> 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.
- We need to add
io.sockets.secure
to ourUSING:
list:USING: calendar.format calendar
io io.server io.sockets io.sockets.secure
io.encodings.ascii ; - We change
daytime-server
to accept SSL connections by replacinginternet-server
withsecure-server
; we also change the port number:: daytime-server ( -- )
9671 secure-server
"daytime"
ascii
[ now timestamp>rfc822 print ]
with-server ;
Thesecure-server
word is much likeinternet-server
in that it takes a port number and outputs a list of addresses, except this time the addresses aresecure
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 } }
} - Next, we change
start-daytime-server
to establish SSL configuration parameters. We need two things here, Diffie-Hellman key exchange parameters, and a certificate for the server.
Diffie-Hellman key exchange parameters can be generated with a command-line tool:openssl dhparam -out dh1024.pem 1024
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 $15 to $3000), or you can generate a self-signed certificate, for example by following these directions. Once you have the two files,dh1024.pem
andserver.pem
, place them insidework/daytime/
, and modifystart-daytime-server
to wrap thedaytime-server
call with thewith-secure-context
combinator. This combinator takes asecure-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 ( -- )
[
<secure-config>
"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 nestwith-secure-context
insidein-thread
, not vice versa, becausein-thread
returns immediately, andwith-secure-context
destroys the context after its quotation returns. - Finally, we change
daytime-client
to establish SSL connections. It suffices to change<inet>
to<inet> <secure>
, and use the new port number we defined indaytime-server
. This makeswith-client
establish an SSL connection:: daytime-client ( hostname -- timestamp )
9671 <inet> <secure> ascii [ readln ] with-client
rfc822>timestamp ;
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:<secure-config> 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 ( -- )
[
<secure-config>
"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 <inet> <secure> ascii [ readln ] with-client
rfc822>timestamp ;
: daytime-client-no-verify ( hostname -- timestamp )
#! Use this if your server has a self-signed certificate
<secure-config> 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:
- As I've mentioned at the beginning, SSL sockets don't yet work on Windows. This will be addressed soon.
- Session resumption is not supported, although this is relatively easy to implement; I just wanted to get everything else working first.
- Client-side authentication isn't supported either. Again, this is pretty easy, and I'll address it soon.
- While it is very easy to create client and server SSL sockets (just wrap your addresses with
<secure>
) there is currently no way to upgrade a socket that has already been opened to an SSL socket. This is needed for SMTP/TLS support. I haven't thought of a nice API for this, as soon as I do I will implement it. - Better control is needed over cypher suites and certificate validation.
2 comments:
Slava, thanks a lot for your encouragement. I know that you'll keep enhancing the library but it's already one of the cleanest implementations I've seen.
Oh sweet, now I just need to bug my webhost into upgrading from FreeBSD 6.3 to 7.x so I can run factor there.
Post a Comment