1// Copyright (c) 2015-2021 MinIO, Inc.
2//
3// This file is part of MinIO Object Storage stack
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18package net
19
20import (
21	"encoding/json"
22	"errors"
23	"net"
24	"regexp"
25	"strings"
26)
27
28var hostLabelRegexp = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$")
29
30// Host - holds network host IP/name and its port.
31type Host struct {
32	Name      string
33	Port      Port
34	IsPortSet bool
35}
36
37// IsEmpty - returns whether Host is empty or not
38func (host Host) IsEmpty() bool {
39	return host.Name == ""
40}
41
42// String - returns string representation of Host.
43func (host Host) String() string {
44	if !host.IsPortSet {
45		return host.Name
46	}
47
48	return net.JoinHostPort(host.Name, host.Port.String())
49}
50
51// Equal - checks whether given host is equal or not.
52func (host Host) Equal(compHost Host) bool {
53	return host.String() == compHost.String()
54}
55
56// MarshalJSON - converts Host into JSON data
57func (host Host) MarshalJSON() ([]byte, error) {
58	return json.Marshal(host.String())
59}
60
61// UnmarshalJSON - parses data into Host.
62func (host *Host) UnmarshalJSON(data []byte) (err error) {
63	var s string
64	if err = json.Unmarshal(data, &s); err != nil {
65		return err
66	}
67
68	// Allow empty string
69	if s == "" {
70		*host = Host{}
71		return nil
72	}
73
74	var h *Host
75	if h, err = ParseHost(s); err != nil {
76		return err
77	}
78
79	*host = *h
80	return nil
81}
82
83// ParseHost - parses string into Host
84func ParseHost(s string) (*Host, error) {
85	if s == "" {
86		return nil, errors.New("invalid argument")
87	}
88	isValidHost := func(host string) bool {
89		if host == "" {
90			return true
91		}
92
93		if ip := net.ParseIP(host); ip != nil {
94			return true
95		}
96
97		// host is not a valid IPv4 or IPv6 address
98		// host may be a hostname
99		// refer https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
100		// why checks are done like below
101		if len(host) < 1 || len(host) > 253 {
102			return false
103		}
104
105		for _, label := range strings.Split(host, ".") {
106			if len(label) < 1 || len(label) > 63 {
107				return false
108			}
109
110			if !hostLabelRegexp.MatchString(label) {
111				return false
112			}
113		}
114
115		return true
116	}
117
118	var port Port
119	var isPortSet bool
120	host, portStr, err := net.SplitHostPort(s)
121	if err != nil {
122		if !strings.Contains(err.Error(), "missing port in address") {
123			return nil, err
124		}
125		host = s
126	} else {
127		if port, err = ParsePort(portStr); err != nil {
128			return nil, err
129		}
130
131		isPortSet = true
132	}
133
134	if host != "" {
135		host, err = trimIPv6(host)
136		if err != nil {
137			return nil, err
138		}
139	}
140
141	// IPv6 requires a link-local address on every network interface.
142	// `%interface` should be preserved.
143	trimmedHost := host
144
145	if i := strings.LastIndex(trimmedHost, "%"); i > -1 {
146		// `%interface` can be skipped for validity check though.
147		trimmedHost = trimmedHost[:i]
148	}
149
150	if !isValidHost(trimmedHost) {
151		return nil, errors.New("invalid hostname")
152	}
153
154	return &Host{
155		Name:      host,
156		Port:      port,
157		IsPortSet: isPortSet,
158	}, nil
159}
160
161// IPv6 can be embedded with square brackets.
162func trimIPv6(host string) (string, error) {
163	// `missing ']' in host` error is already handled in `SplitHostPort`
164	if host[len(host)-1] == ']' {
165		if host[0] != '[' {
166			return "", errors.New("missing '[' in host")
167		}
168		return host[1:][:len(host)-2], nil
169	}
170	return host, nil
171}
172