diff --git a/config.go b/config.go index 9743d4c1b..18ac93074 100644 --- a/config.go +++ b/config.go @@ -46,6 +46,14 @@ const ( // ClusterTypes set of cluster types. var ClusterTypes = []string{ClusterNone, ClusterStatic, ClusterGossip} +// TLSConfig contains TLS configuration +type TLSConfig struct { + // CertificatePath contains the path to the certificate (.crt or .pem file) + CertificatePath string `toml:"certificate-path"` + // CertificateKeyPath contains the path to the certificate key (.key file) + CertificateKeyPath string `toml:"certificate-key-path"` +} + // Config represents the configuration for the command. type Config struct { DataDir string `toml:"data-dir"` @@ -80,6 +88,8 @@ type Config struct { Host string `toml:"host"` PollInterval Duration `toml:"poll-interval"` } `toml:"metric"` + + TLS TLSConfig } // NewConfig returns an instance of Config with default options. @@ -94,6 +104,7 @@ func NewConfig() *Config { c.Cluster.Hosts = []string{} c.AntiEntropy.Interval = Duration(DefaultAntiEntropyInterval) c.Metric.Service = DefaultMetrics + c.TLS = TLSConfig{} return c } @@ -109,7 +120,7 @@ func (c *Config) Validate() error { if err != nil { return err } - if !foundItem(c.Cluster.Hosts, bindWithDefaults) { + if !foundItem(c.Cluster.Hosts, bindWithDefaults.ListenAddress()) { return ErrConfigHostsMissing } } diff --git a/ctl/server.go b/ctl/server.go index e3a2c4156..14920c6a4 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -42,4 +42,6 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { flags.StringVarP(&srv.Config.Metric.Service, "metric.service", "", "nop", "Default URI on which pilosa should listen.") flags.StringVarP(&srv.Config.Metric.Host, "metric.host", "", "", "Default URI to send metrics.") flags.DurationVarP((*time.Duration)(&srv.Config.Metric.PollInterval), "metric.poll-interval", "", time.Minute*0, "Polling interval metrics.") + flags.StringVarP(&srv.Config.TLS.CertificatePath, "tls.certificate-path", "", "", "TLS certificate path (usually has the .crt or .pem extension") + flags.StringVarP(&srv.Config.TLS.CertificateKeyPath, "tls.certificate-key-path", "", "", "TLS certificate key path (usually has the .key extension") } diff --git a/pilosa.go b/pilosa.go index dfa8a615f..aa1378a36 100644 --- a/pilosa.go +++ b/pilosa.go @@ -172,15 +172,6 @@ func ContainsSubstring(a string, list []string) bool { return false } -// NormalizeAddress converts addr into a valid "IP4:port" string. -func NormalizeAddress(addr string) (string, error) { - host, port, err := hostPortWithDefaults(addr) - if err != nil { - return "", err - } - return net.JoinHostPort(HostToIP(host), port), nil -} - // HostToIP converts host to an IP4 address based on net.LookupIP(). func HostToIP(host string) string { // if host is not an IP addr, check net.LookupIP() @@ -201,39 +192,10 @@ func HostToIP(host string) string { // AddressWithDefaults converts addr into a valid address, // using defaults when necessary. -func AddressWithDefaults(addr string) (string, error) { - host, port, err := hostPortWithDefaults(addr) - if err != nil { - return "", err +func AddressWithDefaults(addr string) (*URI, error) { + if addr == "" { + return DefaultURI(), nil + } else { + return NewURIFromAddress(addr) } - return net.JoinHostPort(host, port), nil -} - -// hostPortWithDefaults returns the host and port portions of addr -// using defaults when necessary. -func hostPortWithDefaults(addr string) (host, port string, err error) { - // check for a colon between host and port - if !hasPort(addr) { - addr += ":" - } - - // break into host, port - host, port, err = net.SplitHostPort(addr) - if err != nil { - return host, port, err - } - - // use defaults when not provided - if host == "" { - host = DefaultHost - } - if port == "" { - port = DefaultPort - } - - return host, port, nil -} - -func hasPort(s string) bool { - return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } diff --git a/pilosa_test.go b/pilosa_test.go index 8034451eb..f6ad8154d 100644 --- a/pilosa_test.go +++ b/pilosa_test.go @@ -102,12 +102,13 @@ func TestNormalizeAddress(t *testing.T) { {addr: "[invalid][addr]:port", err: "missing port in address"}, } for _, test := range tests { - actual, err := pilosa.NormalizeAddress(test.addr) + uri, err := pilosa.NewURIFromAddress(test.addr) if err != nil { if !strings.Contains(err.Error(), test.err) { t.Errorf("expected error: %v, but got: %v", test.err, err) } } else { + actual := uri.ListenAddress() if !reflect.DeepEqual(actual, test.expected) { t.Errorf("expected: %v, but got: %v", test.expected, actual) } diff --git a/server.go b/server.go index b785cdda1..bd0a8666d 100644 --- a/server.go +++ b/server.go @@ -15,6 +15,7 @@ package pilosa import ( + "crypto/tls" "errors" "fmt" "io" @@ -60,6 +61,7 @@ type Server struct { // Host is replaced with actual host after opening if port is ":0". Network string Host string + Scheme string Cluster *Cluster // Background monitoring intervals. @@ -67,6 +69,9 @@ type Server struct { PollingInterval time.Duration MetricInterval time.Duration + // TLS configuration + TLS TLSConfig + // Misc options. MaxWritesPerRequest int @@ -99,23 +104,40 @@ func NewServer() *Server { // Open opens and initializes the server. func (s *Server) Open() error { - // Require a port in the hostname. - host, port, err := net.SplitHostPort(s.Host) - if err != nil { - return err - } else if port == "" { - port = DefaultPort + var ln net.Listener + var err error + + // If bind URI has the https scheme, enable TLS + if s.Scheme == "https" { + if s.TLS.CertificatePath == "" { + return errors.New("certificate path is required for TLS sockets") + } + if s.TLS.CertificateKeyPath == "" { + return errors.New("certificate key path is required for TLS sockets") + } + cert, err := tls.LoadX509KeyPair(s.TLS.CertificatePath, s.TLS.CertificateKeyPath) + if err != nil { + return err + } + config := tls.Config{Certificates: []tls.Certificate{cert}} + ln, err = tls.Listen("tcp", s.Host, &config) + if err != nil { + return err + } + } else if s.Scheme == "http" { + // Open HTTP listener to determine port (if specified as :0). + ln, err = net.Listen(s.Network, s.Host) + if err != nil { + return fmt.Errorf("net.Listen: %v", err) + } + } else { + return fmt.Errorf("unsupported scheme: %s", s.Scheme) } - // Open HTTP listener to determine port (if specified as :0). - ln, err := net.Listen(s.Network, ":"+port) - if err != nil { - return fmt.Errorf("net.Listen: %v", err) - } s.ln = ln // Determine hostname based on listening port. - s.Host = net.JoinHostPort(host, strconv.Itoa(s.ln.Addr().(*net.TCPAddr).Port)) + // s.Host = net.JoinHostPort(uri.Host(), strconv.Itoa(s.ln.Addr().(*net.TCPAddr).Port)) // Create local node if no cluster is specified. if len(s.Cluster.Nodes) == 0 { diff --git a/server/server.go b/server/server.go index 157fc69f3..54447dc35 100644 --- a/server/server.go +++ b/server/server.go @@ -23,7 +23,6 @@ import ( "fmt" "io" "math/rand" - "net" "os" "path/filepath" "strconv" @@ -143,7 +142,8 @@ func (m *Command) SetupServer() error { if err != nil { return err } - m.Server.Host = bindWithDefaults + m.Server.Host = bindWithDefaults.ListenAddress() + m.Server.Scheme = bindWithDefaults.Scheme() // Set internal port (string). gossipPortStr := pilosa.DefaultGossipPort @@ -163,11 +163,8 @@ func (m *Command) SetupServer() error { } // get the host portion of addr to use for binding - gossipHost, _, err := net.SplitHostPort(bindWithDefaults) - if err != nil { - gossipHost = m.Config.Bind - } - gossipNodeSet := gossip.NewGossipNodeSet(bindWithDefaults, gossipHost, gossipPort, gossipSeed, m.Server) + gossipHost := bindWithDefaults.Host() + gossipNodeSet := gossip.NewGossipNodeSet(bindWithDefaults.ListenAddress(), gossipHost, gossipPort, gossipSeed, m.Server) m.Server.Cluster.NodeSet = gossipNodeSet m.Server.Broadcaster = gossipNodeSet m.Server.BroadcastReceiver = gossipNodeSet @@ -186,6 +183,7 @@ func (m *Command) SetupServer() error { // Set configuration options. m.Server.AntiEntropyInterval = time.Duration(m.Config.AntiEntropy.Interval) m.Server.Cluster.LongQueryTime = time.Duration(m.Config.Cluster.LongQueryTime) + m.Server.TLS = m.Config.TLS return nil } diff --git a/uri.go b/uri.go new file mode 100644 index 000000000..0c14b6594 --- /dev/null +++ b/uri.go @@ -0,0 +1,136 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package pilosa + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +var addressRegexp = regexp.MustCompile("^(([+a-z]+):\\/\\/)?([0-9a-z.-]+)?(:([0-9]+))?$") + +// URI represents a Pilosa URI. +// A Pilosa URI consists of three parts: +// 1) Scheme: Protocol of the URI. Default: http. +// 2) Host: Hostname or IP URI. Default: localhost. +// 3) Port: Port of the URI. Default: 10101. +// +// All parts of the URI are optional. The following are equivalent: +// http://localhost:10101 +// http://localhost +// http://:10101 +// localhost:10101 +// localhost +// :10101 +type URI struct { + scheme string + host string + port uint16 +} + +// DefaultURI creates and returns the default URI. +func DefaultURI() *URI { + return &URI{ + scheme: "http", + host: "localhost", + port: 10101, + } +} + +// NewURIFromHostPort returns a URI with specified host and port. +func NewURIFromHostPort(host string, port uint16) (*URI, error) { + // TODO: validate host + return &URI{ + scheme: "http", + host: host, + port: port, + }, nil +} + +// NewURIFromAddress parses the passed address and returns a URI. +func NewURIFromAddress(address string) (*URI, error) { + return parseAddress(address) +} + +// Scheme returns the scheme of this URI. +func (u *URI) Scheme() string { + return u.scheme +} + +// Host returns the host of this URI. +func (u *URI) Host() string { + return u.host +} + +// Port returns the port of this URI. +func (u *URI) Port() uint16 { + return u.port +} + +// Normalize returns the address in a form usable by a HTTP client. +func (u *URI) Normalize() string { + scheme := u.scheme + index := strings.Index(scheme, "+") + if index >= 0 { + scheme = scheme[:index] + } + return fmt.Sprintf("%s://%s:%d", scheme, u.host, u.port) +} + +// ListenAddress returns the address suitable for passing to `net.Listener.Listen` +func (u *URI) ListenAddress() string { + return fmt.Sprintf("%s:%d", u.host, u.port) +} + +// Equals returns true if the checked URI is equivalent to this URI. +func (u URI) Equals(other *URI) bool { + if other == nil { + return false + } + return u.scheme == other.scheme && + u.host == other.host && + u.port == other.port +} + +func parseAddress(address string) (uri *URI, err error) { + m := addressRegexp.FindStringSubmatch(address) + if m == nil { + return nil, errors.New("invalid address") + } + scheme := "http" + if m[2] != "" { + scheme = m[2] + } + host := "localhost" + if m[3] != "" { + host = m[3] + } + var port = 10101 + if m[5] != "" { + port, err = strconv.Atoi(m[5]) + if err != nil { + return nil, errors.New("error converting port string to int") + } + } + uri = &URI{ + scheme: scheme, + host: host, + port: uint16(port), + } + return uri, nil +}