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