From 61c5beb4192446454fecc5a597673c22d7c094ad Mon Sep 17 00:00:00 2001 From: Chris Van de Gejuchte Date: Tue, 12 Sep 2017 12:00:04 +0200 Subject: [PATCH] Adds tls support --- README.md | 133 ++++++++++++++++++++++++++++++++++++++++++++- example/tls/tls.go | 118 ++++++++++++++++++++++++++++++++++++++++ redcon.go | 110 +++++++++++++++++++++++++++++++++++-- 3 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 example/tls/tls.go diff --git a/README.md b/README.md index 806f7d9..4cb4721 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Installing go get -u github.com/tidwall/redcon ``` -Example +Examples ------- Here's a full example of a Redis clone that accepts: @@ -123,6 +123,136 @@ func main() { } ``` +The same example is also provided for serving redcon over TLS: + +```sh +go run example/tls/tls.go +``` + +```go +package main + +import ( + "crypto/tls" + "log" + "strings" + "sync" + + "github.com/tidwall/redcon" +) + +const serverKey = ` +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHg+g2unjA5BkDtXSN9ShN7kbPlbCcqcYdDu+QeV8XWuoAoGCCqGSM49 +AwEHoUQDQgAEcZpodWh3SEs5Hh3rrEiu1LZOYSaNIWO34MgRxvqwz1FMpLxNlx0G +cSqrxhPubawptX5MSr02ft32kfOlYbaF5Q== +-----END EC PRIVATE KEY----- +` + +const serverCert = ` +-----BEGIN CERTIFICATE----- +MIIB+TCCAZ+gAwIBAgIJAL05LKXo6PrrMAoGCCqGSM49BAMCMFkxCzAJBgNVBAYT +AkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRn +aXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNTEyMDgxNDAxMTNa +Fw0yNTEyMDUxNDAxMTNaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 +YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMM +CWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHGaaHVod0hLOR4d +66xIrtS2TmEmjSFjt+DIEcb6sM9RTKS8TZcdBnEqq8YT7m2sKbV+TEq9Nn7d9pHz +pWG2heWjUDBOMB0GA1UdDgQWBBR0fqrecDJ44D/fiYJiOeBzfoqEijAfBgNVHSME +GDAWgBR0fqrecDJ44D/fiYJiOeBzfoqEijAMBgNVHRMEBTADAQH/MAoGCCqGSM49 +BAMCA0gAMEUCIEKzVMF3JqjQjuM2rX7Rx8hancI5KJhwfeKu1xbyR7XaAiEA2UT7 +1xOP035EcraRmWPe7tO0LpXgMxlh2VItpc2uc2w= +-----END CERTIFICATE----- +` + +var addr = ":6380" + +func main() { + cer, err := tls.X509KeyPair([]byte(serverCert), []byte(serverKey)) + if err != nil { + log.Fatal(err) + } + config := &tls.Config{Certificates: []tls.Certificate{cer}} + + var mu sync.RWMutex + var items = make(map[string][]byte) + + go log.Printf("started server at %s", addr) + err = redcon.ListenAndServeTLS(addr, + func(conn redcon.Conn, cmd redcon.Command) { + switch strings.ToLower(string(cmd.Args[0])) { + default: + conn.WriteError("ERR unknown command '" + string(cmd.Args[0]) + "'") + case "detach": + hconn := conn.Detach() + log.Printf("connection has been detached") + go func() { + defer hconn.Close() + hconn.WriteString("OK") + hconn.Flush() + }() + return + case "ping": + conn.WriteString("PONG") + case "quit": + conn.WriteString("OK") + conn.Close() + case "set": + if len(cmd.Args) != 3 { + conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") + return + } + mu.Lock() + items[string(cmd.Args[1])] = cmd.Args[2] + mu.Unlock() + conn.WriteString("OK") + case "get": + if len(cmd.Args) != 2 { + conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") + return + } + mu.RLock() + val, ok := items[string(cmd.Args[1])] + mu.RUnlock() + if !ok { + conn.WriteNull() + } else { + conn.WriteBulk(val) + } + case "del": + if len(cmd.Args) != 2 { + conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") + return + } + mu.Lock() + _, ok := items[string(cmd.Args[1])] + delete(items, string(cmd.Args[1])) + mu.Unlock() + if !ok { + conn.WriteInt(0) + } else { + conn.WriteInt(1) + } + } + }, + func(conn redcon.Conn) bool { + return true + }, + func(conn redcon.Conn, err error) { + }, + config, + ) + + if err != nil { + log.Fatal(err) + } +} +``` + + Benchmarks ---------- @@ -168,4 +298,3 @@ Josh Baker [@tidwall](http://twitter.com/tidwall) License ------- Redcon source code is available under the MIT [License](/LICENSE). - diff --git a/example/tls/tls.go b/example/tls/tls.go new file mode 100644 index 0000000..8d1b67c --- /dev/null +++ b/example/tls/tls.go @@ -0,0 +1,118 @@ +package main + +import ( + "crypto/tls" + "log" + "strings" + "sync" + + "github.com/tidwall/redcon" +) + +const serverKey = `-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHg+g2unjA5BkDtXSN9ShN7kbPlbCcqcYdDu+QeV8XWuoAoGCCqGSM49 +AwEHoUQDQgAEcZpodWh3SEs5Hh3rrEiu1LZOYSaNIWO34MgRxvqwz1FMpLxNlx0G +cSqrxhPubawptX5MSr02ft32kfOlYbaF5Q== +-----END EC PRIVATE KEY----- +` + +const serverCert = `-----BEGIN CERTIFICATE----- +MIIB+TCCAZ+gAwIBAgIJAL05LKXo6PrrMAoGCCqGSM49BAMCMFkxCzAJBgNVBAYT +AkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRn +aXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNTEyMDgxNDAxMTNa +Fw0yNTEyMDUxNDAxMTNaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 +YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMM +CWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHGaaHVod0hLOR4d +66xIrtS2TmEmjSFjt+DIEcb6sM9RTKS8TZcdBnEqq8YT7m2sKbV+TEq9Nn7d9pHz +pWG2heWjUDBOMB0GA1UdDgQWBBR0fqrecDJ44D/fiYJiOeBzfoqEijAfBgNVHSME +GDAWgBR0fqrecDJ44D/fiYJiOeBzfoqEijAMBgNVHRMEBTADAQH/MAoGCCqGSM49 +BAMCA0gAMEUCIEKzVMF3JqjQjuM2rX7Rx8hancI5KJhwfeKu1xbyR7XaAiEA2UT7 +1xOP035EcraRmWPe7tO0LpXgMxlh2VItpc2uc2w= +-----END CERTIFICATE----- +` + +var addr = ":6380" + +func main() { + cer, err := tls.X509KeyPair([]byte(serverCert), []byte(serverKey)) + if err != nil { + log.Fatal(err) + } + config := &tls.Config{Certificates: []tls.Certificate{cer}} + + var mu sync.RWMutex + var items = make(map[string][]byte) + + go log.Printf("started server at %s", addr) + err = redcon.ListenAndServeTLS(addr, + func(conn redcon.Conn, cmd redcon.Command) { + switch strings.ToLower(string(cmd.Args[0])) { + default: + conn.WriteError("ERR unknown command '" + string(cmd.Args[0]) + "'") + case "detach": + hconn := conn.Detach() + log.Printf("connection has been detached") + go func() { + defer hconn.Close() + hconn.WriteString("OK") + hconn.Flush() + }() + return + case "ping": + conn.WriteString("PONG") + case "quit": + conn.WriteString("OK") + conn.Close() + case "set": + if len(cmd.Args) != 3 { + conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") + return + } + mu.Lock() + items[string(cmd.Args[1])] = cmd.Args[2] + mu.Unlock() + conn.WriteString("OK") + case "get": + if len(cmd.Args) != 2 { + conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") + return + } + mu.RLock() + val, ok := items[string(cmd.Args[1])] + mu.RUnlock() + if !ok { + conn.WriteNull() + } else { + conn.WriteBulk(val) + } + case "del": + if len(cmd.Args) != 2 { + conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") + return + } + mu.Lock() + _, ok := items[string(cmd.Args[1])] + delete(items, string(cmd.Args[1])) + mu.Unlock() + if !ok { + conn.WriteInt(0) + } else { + conn.WriteInt(1) + } + } + }, + func(conn redcon.Conn) bool { + return true + }, + func(conn redcon.Conn, err error) { + }, + config, + ) + + if err != nil { + log.Fatal(err) + } +} diff --git a/redcon.go b/redcon.go index 26ba003..0a9b56d 100644 --- a/redcon.go +++ b/redcon.go @@ -3,6 +3,7 @@ package redcon import ( "bufio" + "crypto/tls" "errors" "io" "net" @@ -44,7 +45,7 @@ type Conn interface { WriteInt(num int) // WriteInt64 writes a 64-but signed integer to the client. WriteInt64(num int64) - // WriteArray writes an array header. You must then write addtional + // WriteArray writes an array header. You must then write additional // sub-responses to the client to complete the response. // For example to write two strings: // @@ -100,7 +101,7 @@ func NewServer(addr string, return NewServerNetwork("tcp", addr, handler, accept, closed) } -// NewServerNetworkType returns a new Redcon server. The network net must be +// NewServerNetwork returns a new Redcon server. The network net must be // a stream-oriented network: "tcp", "tcp4", "tcp6", "unix" or "unixpacket" func NewServerNetwork( net, laddr string, @@ -122,6 +123,34 @@ func NewServerNetwork( return s } +// NewServerNetworkTLS returns a new TLS Redcon server. The network net must be +// a stream-oriented network: "tcp", "tcp4", "tcp6", "unix" or "unixpacket" +func NewServerNetworkTLS( + net, laddr string, + handler func(conn Conn, cmd Command), + accept func(conn Conn) bool, + closed func(conn Conn, err error), + config *tls.Config, +) *TLSServer { + if handler == nil { + panic("handler is nil") + } + s := Server{ + net: net, + laddr: laddr, + handler: handler, + accept: accept, + closed: closed, + conns: make(map[*conn]bool), + } + + tls := &TLSServer{ + config: config, + Server: &s, + } + return tls +} + // Close stops listening on the TCP address. // Already Accepted connections will be closed. func (s *Server) Close() error { @@ -139,6 +168,23 @@ func (s *Server) ListenAndServe() error { return s.ListenServeAndSignal(nil) } +// Close stops listening on the TCP address. +// Already Accepted connections will be closed. +func (s *TLSServer) Close() error { + s.mu.Lock() + defer s.mu.Unlock() + if s.ln == nil { + return errors.New("not serving") + } + s.done = true + return s.ln.Close() +} + +// ListenAndServe serves incoming connections. +func (s *TLSServer) ListenAndServe() error { + return s.ListenServeAndSignal(nil) +} + // ListenAndServe creates a new server and binds to addr configured on "tcp" network net. func ListenAndServe(addr string, handler func(conn Conn, cmd Command), @@ -148,7 +194,17 @@ func ListenAndServe(addr string, return ListenAndServeNetwork("tcp", addr, handler, accept, closed) } -// ListenAndServe creates a new server and binds to addr. The network net must be +// ListenAndServeTLS creates a new TLS server and binds to addr configured on "tcp" network net. +func ListenAndServeTLS(addr string, + handler func(conn Conn, cmd Command), + accept func(conn Conn) bool, + closed func(conn Conn, err error), + config *tls.Config, +) error { + return ListenAndServeNetworkTLS("tcp", addr, handler, accept, closed, config) +} + +// ListenAndServeNetwork creates a new server and binds to addr. The network net must be // a stream-oriented network: "tcp", "tcp4", "tcp6", "unix" or "unixpacket" func ListenAndServeNetwork( net, laddr string, @@ -159,6 +215,18 @@ func ListenAndServeNetwork( return NewServerNetwork(net, laddr, handler, accept, closed).ListenAndServe() } +// ListenAndServeNetworkTLS creates a new TLS server and binds to addr. The network net must be +// a stream-oriented network: "tcp", "tcp4", "tcp6", "unix" or "unixpacket" +func ListenAndServeNetworkTLS( + net, laddr string, + handler func(conn Conn, cmd Command), + accept func(conn Conn) bool, + closed func(conn Conn, err error), + config *tls.Config, +) error { + return NewServerNetworkTLS(net, laddr, handler, accept, closed, config).ListenAndServe() +} + // ListenServeAndSignal serves incoming connections and passes nil or error // when listening. signal can be nil. func (s *Server) ListenServeAndSignal(signal chan error) error { @@ -172,6 +240,26 @@ func (s *Server) ListenServeAndSignal(signal chan error) error { if signal != nil { signal <- nil } + return serve(s, ln) +} + +// ListenServeAndSignal serves incoming connections and passes nil or error +// when listening. signal can be nil. +func (s *TLSServer) ListenServeAndSignal(signal chan error) error { + ln, err := tls.Listen(s.net, s.laddr, s.config) + if err != nil { + if signal != nil { + signal <- err + } + return err + } + if signal != nil { + signal <- nil + } + return serve(s.Server, ln) +} + +func serve(s *Server, ln net.Listener) error { s.mu.Lock() s.ln = ln s.mu.Unlock() @@ -197,8 +285,12 @@ func (s *Server) ListenServeAndSignal(signal chan error) error { } return err } - c := &conn{conn: lnconn, addr: lnconn.RemoteAddr().String(), - wr: NewWriter(lnconn), rd: NewReader(lnconn)} + c := &conn{ + conn: lnconn, + addr: lnconn.RemoteAddr().String(), + wr: NewWriter(lnconn), + rd: NewReader(lnconn), + } s.mu.Lock() s.conns[c] = true s.mu.Unlock() @@ -397,6 +489,12 @@ type Server struct { done bool } +// TLSServer defines a server for clients for managing client connections. +type TLSServer struct { + *Server + config *tls.Config +} + // Writer allows for writing RESP messages. type Writer struct { w io.Writer @@ -415,7 +513,7 @@ func (w *Writer) WriteNull() { w.b = AppendNull(w.b) } -// WriteArray writes an array header. You must then write addtional +// WriteArray writes an array header. You must then write additional // sub-responses to the client to complete the response. // For example to write two strings: //