1// Package nat is a convenience package for manipulation of strings describing network ports.
2package nat
3
4import (
5	"fmt"
6	"net"
7	"strconv"
8	"strings"
9)
10
11const (
12	// portSpecTemplate is the expected format for port specifications
13	portSpecTemplate = "ip:hostPort:containerPort"
14)
15
16// PortBinding represents a binding between a Host IP address and a Host Port
17type PortBinding struct {
18	// HostIP is the host IP Address
19	HostIP string `json:"HostIp"`
20	// HostPort is the host port number
21	HostPort string
22}
23
24// PortMap is a collection of PortBinding indexed by Port
25type PortMap map[Port][]PortBinding
26
27// PortSet is a collection of structs indexed by Port
28type PortSet map[Port]struct{}
29
30// Port is a string containing port number and protocol in the format "80/tcp"
31type Port string
32
33// NewPort creates a new instance of a Port given a protocol and port number or port range
34func NewPort(proto, port string) (Port, error) {
35	// Check for parsing issues on "port" now so we can avoid having
36	// to check it later on.
37
38	portStartInt, portEndInt, err := ParsePortRangeToInt(port)
39	if err != nil {
40		return "", err
41	}
42
43	if portStartInt == portEndInt {
44		return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil
45	}
46	return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil
47}
48
49// ParsePort parses the port number string and returns an int
50func ParsePort(rawPort string) (int, error) {
51	if len(rawPort) == 0 {
52		return 0, nil
53	}
54	port, err := strconv.ParseUint(rawPort, 10, 16)
55	if err != nil {
56		return 0, err
57	}
58	return int(port), nil
59}
60
61// ParsePortRangeToInt parses the port range string and returns start/end ints
62func ParsePortRangeToInt(rawPort string) (int, int, error) {
63	if len(rawPort) == 0 {
64		return 0, 0, nil
65	}
66	start, end, err := ParsePortRange(rawPort)
67	if err != nil {
68		return 0, 0, err
69	}
70	return int(start), int(end), nil
71}
72
73// Proto returns the protocol of a Port
74func (p Port) Proto() string {
75	proto, _ := SplitProtoPort(string(p))
76	return proto
77}
78
79// Port returns the port number of a Port
80func (p Port) Port() string {
81	_, port := SplitProtoPort(string(p))
82	return port
83}
84
85// Int returns the port number of a Port as an int
86func (p Port) Int() int {
87	portStr := p.Port()
88	// We don't need to check for an error because we're going to
89	// assume that any error would have been found, and reported, in NewPort()
90	port, _ := ParsePort(portStr)
91	return port
92}
93
94// Range returns the start/end port numbers of a Port range as ints
95func (p Port) Range() (int, int, error) {
96	return ParsePortRangeToInt(p.Port())
97}
98
99// SplitProtoPort splits a port in the format of proto/port
100func SplitProtoPort(rawPort string) (string, string) {
101	parts := strings.Split(rawPort, "/")
102	l := len(parts)
103	if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 {
104		return "", ""
105	}
106	if l == 1 {
107		return "tcp", rawPort
108	}
109	if len(parts[1]) == 0 {
110		return "tcp", parts[0]
111	}
112	return parts[1], parts[0]
113}
114
115func validateProto(proto string) bool {
116	for _, availableProto := range []string{"tcp", "udp"} {
117		if availableProto == proto {
118			return true
119		}
120	}
121	return false
122}
123
124// ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses
125// these in to the internal types
126func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error) {
127	var (
128		exposedPorts = make(map[Port]struct{}, len(ports))
129		bindings     = make(map[Port][]PortBinding)
130	)
131	for _, rawPort := range ports {
132		portMappings, err := ParsePortSpec(rawPort)
133		if err != nil {
134			return nil, nil, err
135		}
136
137		for _, portMapping := range portMappings {
138			port := portMapping.Port
139			if _, exists := exposedPorts[port]; !exists {
140				exposedPorts[port] = struct{}{}
141			}
142			bslice, exists := bindings[port]
143			if !exists {
144				bslice = []PortBinding{}
145			}
146			bindings[port] = append(bslice, portMapping.Binding)
147		}
148	}
149	return exposedPorts, bindings, nil
150}
151
152// PortMapping is a data object mapping a Port to a PortBinding
153type PortMapping struct {
154	Port    Port
155	Binding PortBinding
156}
157
158func splitParts(rawport string) (string, string, string) {
159	parts := strings.Split(rawport, ":")
160	n := len(parts)
161	containerport := parts[n-1]
162
163	switch n {
164	case 1:
165		return "", "", containerport
166	case 2:
167		return "", parts[0], containerport
168	case 3:
169		return parts[0], parts[1], containerport
170	default:
171		return strings.Join(parts[:n-2], ":"), parts[n-2], containerport
172	}
173}
174
175// ParsePortSpec parses a port specification string into a slice of PortMappings
176func ParsePortSpec(rawPort string) ([]PortMapping, error) {
177	var proto string
178	rawIP, hostPort, containerPort := splitParts(rawPort)
179	proto, containerPort = SplitProtoPort(containerPort)
180
181	// Strip [] from IPV6 addresses
182	ip, _, err := net.SplitHostPort(rawIP + ":")
183	if err != nil {
184		return nil, fmt.Errorf("Invalid ip address %v: %s", rawIP, err)
185	}
186	if ip != "" && net.ParseIP(ip) == nil {
187		return nil, fmt.Errorf("Invalid ip address: %s", ip)
188	}
189	if containerPort == "" {
190		return nil, fmt.Errorf("No port specified: %s<empty>", rawPort)
191	}
192
193	startPort, endPort, err := ParsePortRange(containerPort)
194	if err != nil {
195		return nil, fmt.Errorf("Invalid containerPort: %s", containerPort)
196	}
197
198	var startHostPort, endHostPort uint64 = 0, 0
199	if len(hostPort) > 0 {
200		startHostPort, endHostPort, err = ParsePortRange(hostPort)
201		if err != nil {
202			return nil, fmt.Errorf("Invalid hostPort: %s", hostPort)
203		}
204	}
205
206	if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
207		// Allow host port range iff containerPort is not a range.
208		// In this case, use the host port range as the dynamic
209		// host port range to allocate into.
210		if endPort != startPort {
211			return nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
212		}
213	}
214
215	if !validateProto(strings.ToLower(proto)) {
216		return nil, fmt.Errorf("Invalid proto: %s", proto)
217	}
218
219	ports := []PortMapping{}
220	for i := uint64(0); i <= (endPort - startPort); i++ {
221		containerPort = strconv.FormatUint(startPort+i, 10)
222		if len(hostPort) > 0 {
223			hostPort = strconv.FormatUint(startHostPort+i, 10)
224		}
225		// Set hostPort to a range only if there is a single container port
226		// and a dynamic host port.
227		if startPort == endPort && startHostPort != endHostPort {
228			hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10))
229		}
230		port, err := NewPort(strings.ToLower(proto), containerPort)
231		if err != nil {
232			return nil, err
233		}
234
235		binding := PortBinding{
236			HostIP:   ip,
237			HostPort: hostPort,
238		}
239		ports = append(ports, PortMapping{Port: port, Binding: binding})
240	}
241	return ports, nil
242}
243