1package ice
2
3import (
4	"net"
5	"net/url"
6	"strconv"
7)
8
9// SchemeType indicates the type of server used in the ice.URL structure.
10type SchemeType int
11
12// Unknown defines default public constant to use for "enum" like struct
13// comparisons when no value was defined.
14const Unknown = iota
15
16const (
17	// SchemeTypeSTUN indicates the URL represents a STUN server.
18	SchemeTypeSTUN SchemeType = iota + 1
19
20	// SchemeTypeSTUNS indicates the URL represents a STUNS (secure) server.
21	SchemeTypeSTUNS
22
23	// SchemeTypeTURN indicates the URL represents a TURN server.
24	SchemeTypeTURN
25
26	// SchemeTypeTURNS indicates the URL represents a TURNS (secure) server.
27	SchemeTypeTURNS
28)
29
30// NewSchemeType defines a procedure for creating a new SchemeType from a raw
31// string naming the scheme type.
32func NewSchemeType(raw string) SchemeType {
33	switch raw {
34	case "stun":
35		return SchemeTypeSTUN
36	case "stuns":
37		return SchemeTypeSTUNS
38	case "turn":
39		return SchemeTypeTURN
40	case "turns":
41		return SchemeTypeTURNS
42	default:
43		return SchemeType(Unknown)
44	}
45}
46
47func (t SchemeType) String() string {
48	switch t {
49	case SchemeTypeSTUN:
50		return "stun"
51	case SchemeTypeSTUNS:
52		return "stuns"
53	case SchemeTypeTURN:
54		return "turn"
55	case SchemeTypeTURNS:
56		return "turns"
57	default:
58		return ErrUnknownType.Error()
59	}
60}
61
62// ProtoType indicates the transport protocol type that is used in the ice.URL
63// structure.
64type ProtoType int
65
66const (
67	// ProtoTypeUDP indicates the URL uses a UDP transport.
68	ProtoTypeUDP ProtoType = iota + 1
69
70	// ProtoTypeTCP indicates the URL uses a TCP transport.
71	ProtoTypeTCP
72)
73
74// NewProtoType defines a procedure for creating a new ProtoType from a raw
75// string naming the transport protocol type.
76func NewProtoType(raw string) ProtoType {
77	switch raw {
78	case "udp":
79		return ProtoTypeUDP
80	case "tcp":
81		return ProtoTypeTCP
82	default:
83		return ProtoType(Unknown)
84	}
85}
86
87func (t ProtoType) String() string {
88	switch t {
89	case ProtoTypeUDP:
90		return "udp"
91	case ProtoTypeTCP:
92		return "tcp"
93	default:
94		return ErrUnknownType.Error()
95	}
96}
97
98// URL represents a STUN (rfc7064) or TURN (rfc7065) URL
99type URL struct {
100	Scheme   SchemeType
101	Host     string
102	Port     int
103	Username string
104	Password string
105	Proto    ProtoType
106}
107
108// ParseURL parses a STUN or TURN urls following the ABNF syntax described in
109// https://tools.ietf.org/html/rfc7064 and https://tools.ietf.org/html/rfc7065
110// respectively.
111func ParseURL(raw string) (*URL, error) { //nolint:gocognit
112	rawParts, err := url.Parse(raw)
113	if err != nil {
114		return nil, err
115	}
116
117	var u URL
118	u.Scheme = NewSchemeType(rawParts.Scheme)
119	if u.Scheme == SchemeType(Unknown) {
120		return nil, ErrSchemeType
121	}
122
123	var rawPort string
124	if u.Host, rawPort, err = net.SplitHostPort(rawParts.Opaque); err != nil {
125		if e, ok := err.(*net.AddrError); ok {
126			if e.Err == "missing port in address" {
127				nextRawURL := u.Scheme.String() + ":" + rawParts.Opaque
128				switch {
129				case u.Scheme == SchemeTypeSTUN || u.Scheme == SchemeTypeTURN:
130					nextRawURL += ":3478"
131					if rawParts.RawQuery != "" {
132						nextRawURL += "?" + rawParts.RawQuery
133					}
134					return ParseURL(nextRawURL)
135				case u.Scheme == SchemeTypeSTUNS || u.Scheme == SchemeTypeTURNS:
136					nextRawURL += ":5349"
137					if rawParts.RawQuery != "" {
138						nextRawURL += "?" + rawParts.RawQuery
139					}
140					return ParseURL(nextRawURL)
141				}
142			}
143		}
144		return nil, err
145	}
146
147	if u.Host == "" {
148		return nil, ErrHost
149	}
150
151	if u.Port, err = strconv.Atoi(rawPort); err != nil {
152		return nil, ErrPort
153	}
154
155	switch u.Scheme {
156	case SchemeTypeSTUN:
157		qArgs, err := url.ParseQuery(rawParts.RawQuery)
158		if err != nil || len(qArgs) > 0 {
159			return nil, ErrSTUNQuery
160		}
161		u.Proto = ProtoTypeUDP
162	case SchemeTypeSTUNS:
163		qArgs, err := url.ParseQuery(rawParts.RawQuery)
164		if err != nil || len(qArgs) > 0 {
165			return nil, ErrSTUNQuery
166		}
167		u.Proto = ProtoTypeTCP
168	case SchemeTypeTURN:
169		proto, err := parseProto(rawParts.RawQuery)
170		if err != nil {
171			return nil, err
172		}
173
174		u.Proto = proto
175		if u.Proto == ProtoType(Unknown) {
176			u.Proto = ProtoTypeUDP
177		}
178	case SchemeTypeTURNS:
179		proto, err := parseProto(rawParts.RawQuery)
180		if err != nil {
181			return nil, err
182		}
183
184		u.Proto = proto
185		if u.Proto == ProtoType(Unknown) {
186			u.Proto = ProtoTypeTCP
187		}
188	}
189
190	return &u, nil
191}
192
193func parseProto(raw string) (ProtoType, error) {
194	qArgs, err := url.ParseQuery(raw)
195	if err != nil || len(qArgs) > 1 {
196		return ProtoType(Unknown), ErrInvalidQuery
197	}
198
199	var proto ProtoType
200	if rawProto := qArgs.Get("transport"); rawProto != "" {
201		if proto = NewProtoType(rawProto); proto == ProtoType(0) {
202			return ProtoType(Unknown), ErrProtoType
203		}
204		return proto, nil
205	}
206
207	if len(qArgs) > 0 {
208		return ProtoType(Unknown), ErrInvalidQuery
209	}
210
211	return proto, nil
212}
213
214func (u URL) String() string {
215	rawURL := u.Scheme.String() + ":" + net.JoinHostPort(u.Host, strconv.Itoa(u.Port))
216	if u.Scheme == SchemeTypeTURN || u.Scheme == SchemeTypeTURNS {
217		rawURL += "?transport=" + u.Proto.String()
218	}
219	return rawURL
220}
221
222// IsSecure returns whether the this URL's scheme describes secure scheme or not.
223func (u URL) IsSecure() bool {
224	return u.Scheme == SchemeTypeSTUNS || u.Scheme == SchemeTypeTURNS
225}
226