1package connect
2
3import (
4	"fmt"
5	"net/url"
6	"regexp"
7	"strings"
8)
9
10// CertURI represents a Connect-valid URI value for a TLS certificate.
11// The user should type switch on the various implementations in this
12// package to determine the type of URI and the data encoded within it.
13//
14// Note that the current implementations of this are all also SPIFFE IDs.
15// However, we anticipate that we may accept URIs that are also not SPIFFE
16// compliant and therefore the interface is named as such.
17type CertURI interface {
18	// URI is the valid URI value used in the cert.
19	URI() *url.URL
20}
21
22var (
23	spiffeIDServiceRegexp = regexp.MustCompile(
24		`^/ns/([^/]+)/dc/([^/]+)/svc/([^/]+)$`)
25	spiffeIDAgentRegexp = regexp.MustCompile(
26		`^/agent/client/dc/([^/]+)/id/([^/]+)$`)
27)
28
29// ParseCertURIFromString attempts to parse a string representation of a
30// certificate URI as a convenience helper around ParseCertURI.
31func ParseCertURIFromString(input string) (CertURI, error) {
32	// Parse the certificate URI from the string
33	uriRaw, err := url.Parse(input)
34	if err != nil {
35		return nil, err
36	}
37	return ParseCertURI(uriRaw)
38}
39
40// ParseCertURI parses a the URI value from a TLS certificate.
41func ParseCertURI(input *url.URL) (CertURI, error) {
42	if input.Scheme != "spiffe" {
43		return nil, fmt.Errorf("SPIFFE ID must have 'spiffe' scheme")
44	}
45
46	// Path is the raw value of the path without url decoding values.
47	// RawPath is empty if there were no encoded values so we must
48	// check both.
49	path := input.Path
50	if input.RawPath != "" {
51		path = input.RawPath
52	}
53
54	// Test for service IDs
55	if v := spiffeIDServiceRegexp.FindStringSubmatch(path); v != nil {
56		// Determine the values. We assume they're sane to save cycles,
57		// but if the raw path is not empty that means that something is
58		// URL encoded so we go to the slow path.
59		ns := v[1]
60		dc := v[2]
61		service := v[3]
62		if input.RawPath != "" {
63			var err error
64			if ns, err = url.PathUnescape(v[1]); err != nil {
65				return nil, fmt.Errorf("Invalid namespace: %s", err)
66			}
67			if dc, err = url.PathUnescape(v[2]); err != nil {
68				return nil, fmt.Errorf("Invalid datacenter: %s", err)
69			}
70			if service, err = url.PathUnescape(v[3]); err != nil {
71				return nil, fmt.Errorf("Invalid service: %s", err)
72			}
73		}
74
75		return &SpiffeIDService{
76			Host:       input.Host,
77			Namespace:  ns,
78			Datacenter: dc,
79			Service:    service,
80		}, nil
81	} else if v := spiffeIDAgentRegexp.FindStringSubmatch(path); v != nil {
82		// Determine the values. We assume they're sane to save cycles,
83		// but if the raw path is not empty that means that something is
84		// URL encoded so we go to the slow path.
85		dc := v[1]
86		agent := v[2]
87		if input.RawPath != "" {
88			var err error
89			if dc, err = url.PathUnescape(v[1]); err != nil {
90				return nil, fmt.Errorf("Invalid datacenter: %s", err)
91			}
92			if agent, err = url.PathUnescape(v[2]); err != nil {
93				return nil, fmt.Errorf("Invalid node: %s", err)
94			}
95		}
96
97		return &SpiffeIDAgent{
98			Host:       input.Host,
99			Datacenter: dc,
100			Agent:      agent,
101		}, nil
102	}
103
104	// Test for signing ID
105	if input.Path == "" {
106		idx := strings.Index(input.Host, ".")
107		if idx > 0 {
108			return &SpiffeIDSigning{
109				ClusterID: input.Host[:idx],
110				Domain:    input.Host[idx+1:],
111			}, nil
112		}
113	}
114
115	return nil, fmt.Errorf("SPIFFE ID is not in the expected format: %s", input.String())
116}
117