Add endpointaddr pkg for parsing host+port inputs.
This type of field appears in more than one of our APIs, so this package will provide a single source of truth for validating and parsing inputs. Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
parent
61362f8187
commit
d9a3992b3b
71
internal/endpointaddr/endpointaddr.go
Normal file
71
internal/endpointaddr/endpointaddr.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package endpointaddr implements parsing and validation of "<host>[:<port>]" strings for Pinniped APIs.
|
||||||
|
package endpointaddr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HostPort struct {
|
||||||
|
// Host is the validated host part of the input, which may be a hostname or IP.
|
||||||
|
//
|
||||||
|
// This string can be be used as an x509 certificate SAN.
|
||||||
|
Host string
|
||||||
|
|
||||||
|
// Port is the validated port number, which may be defaulted.
|
||||||
|
Port uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint is the host:port validated from the input, where port may be a default value.
|
||||||
|
//
|
||||||
|
// This string can be passed to net.Dial.
|
||||||
|
func (h *HostPort) Endpoint() string {
|
||||||
|
return net.JoinHostPort(h.Host, strconv.Itoa(int(h.Port)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse an "endpoint address" string, providing a default port. The input can be in several valid formats:
|
||||||
|
//
|
||||||
|
// - "<hostname>" (DNS hostname)
|
||||||
|
// - "<IPv4>" (IPv4 address)
|
||||||
|
// - "<IPv6>" (IPv6 address)
|
||||||
|
// - "<hostname>:<port>" (DNS hostname with port)
|
||||||
|
// - "<IPv4>:<port>" (IPv4 address with port)
|
||||||
|
// - "[<IPv6>]:<port>" (IPv6 address with port, brackets are required)
|
||||||
|
//
|
||||||
|
// If the input does not not specify a port number, then defaultPort will be used.
|
||||||
|
func Parse(endpoint string, defaultPort uint16) (HostPort, error) {
|
||||||
|
// Try parsing it both with and without an implicit port 443 at the end.
|
||||||
|
host, port, err := net.SplitHostPort(endpoint)
|
||||||
|
|
||||||
|
// If we got an error parsing the raw input, try adding the default port.
|
||||||
|
if err != nil {
|
||||||
|
host, port, err = net.SplitHostPort(net.JoinHostPort(endpoint, strconv.Itoa(int(defaultPort))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give up if there's still an error splitting the host and port.
|
||||||
|
if err != nil {
|
||||||
|
return HostPort{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the port number is an integer in the range of valid ports.
|
||||||
|
integerPort, _ := strconv.Atoi(port)
|
||||||
|
if len(validation.IsValidPortNum(integerPort)) > 0 {
|
||||||
|
return HostPort{}, fmt.Errorf("invalid port %q", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the host part is a IPv4 or IPv6 address or a valid hostname according to RFC 1123.
|
||||||
|
switch {
|
||||||
|
case len(validation.IsValidIP(host)) == 0:
|
||||||
|
case len(validation.IsDNS1123Subdomain(host)) == 0:
|
||||||
|
default:
|
||||||
|
return HostPort{}, fmt.Errorf("host %q is not a valid hostname or IP address", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return HostPort{Host: host, Port: uint16(integerPort)}, nil
|
||||||
|
}
|
182
internal/endpointaddr/endpointaddr_test.go
Normal file
182
internal/endpointaddr/endpointaddr_test.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package endpointaddr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
defaultPort uint16
|
||||||
|
expectErr string
|
||||||
|
expect HostPort
|
||||||
|
expectEndpoint string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "plain IPv4",
|
||||||
|
input: "127.0.0.1",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "127.0.0.1", Port: 443},
|
||||||
|
expectEndpoint: "127.0.0.1:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 with port",
|
||||||
|
input: "127.0.0.1:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "127.0.0.1", Port: 8443},
|
||||||
|
expectEndpoint: "127.0.0.1:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 in brackets with port",
|
||||||
|
input: "[127.0.0.1]:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "127.0.0.1", Port: 8443},
|
||||||
|
expectEndpoint: "127.0.0.1:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 as IPv6 in brackets with port",
|
||||||
|
input: "[::127.0.0.1]:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "::127.0.0.1", Port: 8443},
|
||||||
|
expectEndpoint: "[::127.0.0.1]:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv4 as IPv6 without port",
|
||||||
|
input: "::127.0.0.1",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "::127.0.0.1", Port: 443},
|
||||||
|
expectEndpoint: "[::127.0.0.1]:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain IPv6 without port",
|
||||||
|
input: "2001:db8::ffff",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "2001:db8::ffff", Port: 443},
|
||||||
|
expectEndpoint: "[2001:db8::ffff]:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 with port",
|
||||||
|
input: "[2001:db8::ffff]:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "2001:db8::ffff", Port: 8443},
|
||||||
|
expectEndpoint: "[2001:db8::ffff]:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain hostname",
|
||||||
|
input: "host.example.com",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "host.example.com", Port: 443},
|
||||||
|
expectEndpoint: "host.example.com:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plain hostname with dash",
|
||||||
|
input: "host-dev.example.com",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "host-dev.example.com", Port: 443},
|
||||||
|
expectEndpoint: "host-dev.example.com:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname with port",
|
||||||
|
input: "host.example.com:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "host.example.com", Port: 8443},
|
||||||
|
expectEndpoint: "host.example.com:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname in brackets with port",
|
||||||
|
input: "[host.example.com]:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "host.example.com", Port: 8443},
|
||||||
|
expectEndpoint: "host.example.com:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname without dots",
|
||||||
|
input: "localhost",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "localhost", Port: 443},
|
||||||
|
expectEndpoint: "localhost:443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "hostname and port without dots",
|
||||||
|
input: "localhost:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expect: HostPort{Host: "localhost", Port: 8443},
|
||||||
|
expectEndpoint: "localhost:8443",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid empty string",
|
||||||
|
input: "",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `host "" is not a valid hostname or IP address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// IPv6 zone index specifiers are not yet supported.
|
||||||
|
name: "IPv6 with port and zone index",
|
||||||
|
input: "[2001:db8::ffff%lo0]:8443",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `host "2001:db8::ffff%lo0" is not a valid hostname or IP address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 in brackets without port",
|
||||||
|
input: "[2001:db8::ffff]",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `address [[2001:db8::ffff]]:443: missing port in address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid HTTPS URL",
|
||||||
|
input: "https://host.example.com",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `invalid port "//host.example.com"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host with URL path",
|
||||||
|
input: "host.example.com/some/path",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `host "host.example.com/some/path" is not a valid hostname or IP address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host with mismatched brackets",
|
||||||
|
input: "[host.example.com",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: "address [host.example.com:443: missing ']' in address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host with underscores",
|
||||||
|
input: "___.example.com:1234",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `host "___.example.com" is not a valid hostname or IP address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host with uppercase",
|
||||||
|
input: "HOST.EXAMPLE.COM",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `host "HOST.EXAMPLE.COM" is not a valid hostname or IP address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid host with extra port",
|
||||||
|
input: "host.example.com:port1:port2",
|
||||||
|
defaultPort: 443,
|
||||||
|
expectErr: `host "host.example.com:port1:port2" is not a valid hostname or IP address`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Parse(tt.input, tt.defaultPort)
|
||||||
|
if tt.expectErr == "" {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expect, got)
|
||||||
|
assert.Equal(t, tt.expectEndpoint, got.Endpoint())
|
||||||
|
} else {
|
||||||
|
assert.EqualError(t, err, tt.expectErr)
|
||||||
|
assert.Equal(t, HostPort{}, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user