Initial TLS support

This commit is contained in:
Yuce Tekol 2017-10-02 15:53:50 +03:00
parent 162b084a82
commit 5760d89742
No known key found for this signature in database
GPG key ID: CB59E46D2FB90573
7 changed files with 196 additions and 64 deletions

View file

@ -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
}
}

View file

@ -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")
}

View file

@ -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, "]")
}

View file

@ -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)
}

View file

@ -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 {

View file

@ -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
}

136
uri.go Normal file
View file

@ -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
}