summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/cap.go
blob: 631b925baa08b9505bedc679e9ac632ae398ad45 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
// of this source code is governed by the MIT license that can be found in
// the LICENSE file.

package girc

import (
	"fmt"
	"strconv"
	"strings"
	"time"
)

// Something not in the list? Depending on the type of capability, you can
// enable it using Config.SupportedCaps.
var possibleCap = map[string][]string{
	"account-notify":    nil,
	"account-tag":       nil,
	"away-notify":       nil,
	"batch":             nil,
	"cap-notify":        nil,
	"chghost":           nil,
	"extended-join":     nil,
	"invite-notify":     nil,
	"message-tags":      nil,
	"msgid":             nil,
	"multi-prefix":      nil,
	"server-time":       nil,
	"userhost-in-names": nil,

	// Supported draft versions, some may be duplicated above, this is for backwards
	// compatibility.
	"draft/message-tags-0.2": nil,
	"draft/msgid":            nil,

	// sts, sasl, etc are enabled dynamically/depending on client configuration,
	// so aren't included on this list.

	// "echo-message" is supported, but it's not enabled by default. This is
	// to prevent unwanted confusion and utilize less traffic if it's not needed.
	// echo messages aren't sent to girc.PRIVMSG and girc.NOTICE handlers,
	// rather they are only sent to girc.ALL_EVENTS handlers (this is to prevent
	// each handler to have to check these types of things for each message).
	// You can compare events using Event.Equals() to see if they are the same.
}

// https://ircv3.net/specs/extensions/server-time-3.2.html
// <value> ::= YYYY-MM-DDThh:mm:ss.sssZ
const capServerTimeFormat = "2006-01-02T15:04:05.999Z"

func (c *Client) listCAP() {
	if !c.Config.disableTracking {
		c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}})
	}
}

func possibleCapList(c *Client) map[string][]string {
	out := make(map[string][]string)

	if c.Config.SASL != nil {
		out["sasl"] = nil
	}

	if !c.Config.DisableSTS && !c.Config.SSL {
		// If fallback supported, and we failed recently, don't try negotiating STS.
		// ONLY do this fallback if we're expired (primarily useful during the first
		// sts negotiation).
		if time.Since(c.state.sts.lastFailed) < 5*time.Minute && !c.Config.DisableSTSFallback {
			c.debug.Println("skipping strict transport policy negotiation; failed within the last 5 minutes")
		} else {
			out["sts"] = nil
		}
	}

	for k := range c.Config.SupportedCaps {
		out[k] = c.Config.SupportedCaps[k]
	}

	for k := range possibleCap {
		out[k] = possibleCap[k]
	}

	return out
}

func parseCap(raw string) map[string]map[string]string {
	out := make(map[string]map[string]string)
	parts := strings.Split(raw, " ")

	var val int

	for i := 0; i < len(parts); i++ {
		val = strings.IndexByte(parts[i], prefixTagValue) // =

		// No value splitter, or has splitter but no trailing value.
		if val < 1 || len(parts[i]) < val+1 {
			// The capability doesn't contain a value.
			out[parts[i]] = nil
			continue
		}

		out[parts[i][:val]] = make(map[string]string)
		for _, option := range strings.Split(parts[i][val+1:], ",") {
			j := strings.Index(option, "=")

			if j < 0 {
				out[parts[i][:val]][option] = ""
			} else {
				out[parts[i][:val]][option[:j]] = option[j+1:]
			}
		}
	}

	return out
}

// handleCAP attempts to find out what IRCv3 capabilities the server supports.
// This will lock further registration until we have acknowledged (or denied)
// the capabilities.
func handleCAP(c *Client, e Event) {
	c.state.Lock()
	defer c.state.Unlock()

	if len(e.Params) >= 2 && e.Params[1] == CAP_DEL {
		caps := parseCap(e.Last())
		for cap := range caps {
			// TODO: test the deletion.
			delete(c.state.enabledCap, cap)
		}
		return
	}

	// We can assume there was a failure attempting to enable a capability.
	if len(e.Params) >= 2 && e.Params[1] == CAP_NAK {
		// Let the server know that we're done.
		c.write(&Event{Command: CAP, Params: []string{CAP_END}})
		return
	}

	possible := possibleCapList(c)
	// TODO: test the addition.
	if len(e.Params) >= 3 && (e.Params[1] == CAP_LS || e.Params[1] == CAP_NEW) {
		caps := parseCap(e.Last())

		for capName := range caps {
			if _, ok := possible[capName]; !ok {
				continue
			}

			if len(possible[capName]) == 0 || len(caps[capName]) == 0 {
				c.state.tmpCap[capName] = caps[capName]
				continue
			}

			var contains bool

			for capAttr := range caps[capName] {
				for i := 0; i < len(possible[capName]); i++ {
					if _, ok := caps[capName][capAttr]; ok {
						// Assuming we have a matching attribute for the capability.
						contains = true
						goto checkcontains
					}
				}
			}

		checkcontains:
			if !contains {
				continue
			}

			c.state.tmpCap[capName] = caps[capName]
		}

		// Indicates if this is a multi-line LS. (3 args means it's the
		// last LS).
		if len(e.Params) == 3 {
			// If we support no caps, just ack the CAP message and END.
			if len(c.state.tmpCap) == 0 {
				c.write(&Event{Command: CAP, Params: []string{CAP_END}})
				return
			}

			// Let them know which ones we'd like to enable.
			reqKeys := make([]string, len(c.state.tmpCap))
			i := 0
			for k := range c.state.tmpCap {
				reqKeys[i] = k
				i++
			}
			c.write(&Event{Command: CAP, Params: []string{CAP_REQ, strings.Join(reqKeys, " ")}})
		}
	}

	if len(e.Params) == 3 && e.Params[1] == CAP_ACK {
		enabled := strings.Split(e.Last(), " ")
		for _, cap := range enabled {
			if val, ok := c.state.tmpCap[cap]; ok {
				c.state.enabledCap[cap] = val
			} else {
				c.state.enabledCap[cap] = nil
			}
		}

		// Anything client side that needs to be setup post-capability-acknowledgement,
		// should be done here.

		// Handle STS, and only if it's something specifically we enabled (client
		// may choose to disable girc automatic STS, and do it themselves).
		if sts, sok := c.state.enabledCap["sts"]; sok && !c.Config.DisableSTS {
			var isError bool

			// Some things are updated in the policy depending on if the current
			// connection is over tls or not.
			var hasTLSConnection bool
			if tlsState, _ := c.TLSConnectionState(); tlsState != nil {
				hasTLSConnection = true
			}

			// "This key indicates the port number for making a secure connection.
			// This key’s value MUST be a single port number. If the client is not
			// already connected securely to the server at the requested hostname,
			// it MUST close the insecure connection and reconnect securely on the
			// stated port.
			//
			// To enforce an STS upgrade policy, servers MUST send this key to
			// insecurely connected clients. Servers MAY send this key to securely
			// connected clients, but it will be ignored."
			//
			// See: https://ircv3.net/specs/extensions/sts#the-port-key
			if !hasTLSConnection {
				if port, ok := sts["port"]; ok {
					c.state.sts.upgradePort, _ = strconv.Atoi(port)
					if c.state.sts.upgradePort < 21 {
						isError = true
					}
				} else {
					isError = true
				}
			}

			// "This key is used on secure connections to indicate how long clients
			// MUST continue to use secure connections when connecting to the server
			// at the requested hostname. The value of this key MUST be given as a
			// single integer which represents the number of seconds until the persistence
			// policy expires.
			//
			// To enforce an STS persistence policy, servers MUST send this key to
			// securely connected clients. Servers MAY send this key to all clients,
			// but insecurely connected clients MUST ignore it."
			//
			// See: https://ircv3.net/specs/extensions/sts#the-duration-key
			if hasTLSConnection {
				if duration, ok := sts["duration"]; ok {
					c.state.sts.persistenceDuration, _ = strconv.Atoi(duration)
					c.state.sts.persistenceReceived = time.Now()
				} else {
					isError = true
				}
			}

			// See: https://ircv3.net/specs/extensions/sts#the-preload-key
			if hasTLSConnection {
				if preload, ok := sts["preload"]; ok {
					c.state.sts.preload, _ = strconv.ParseBool(preload)
				}
			}

			if isError {
				c.rx <- &Event{Command: ERROR, Params: []string{
					fmt.Sprintf("closing connection: strict transport policy provided by server is invalid; possible MITM? config: %#v", sts),
				}}
				return
			}

			// Only upgrade if not already upgraded.
			if !hasTLSConnection {
				c.state.sts.beginUpgrade = true

				c.RunHandlers(&Event{Command: STS_UPGRADE_INIT})
				c.debug.Println("strict transport security policy provided by server; closing connection to begin upgrade...")
				c.Close()
				return
			}
		}

		// Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests
		// due to cap-notify, we can re-evaluate what we can support.
		c.state.tmpCap = make(map[string]map[string]string)

		if _, ok := c.state.enabledCap["sasl"]; ok && c.Config.SASL != nil {
			c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}})
			// Don't "CAP END", since we want to authenticate.
			return
		}

		// Let the server know that we're done.
		c.write(&Event{Command: CAP, Params: []string{CAP_END}})
		return
	}
}

// handleCHGHOST handles incoming IRCv3 hostname change events. CHGHOST is
// what occurs (when enabled) when a servers services change the hostname of
// a user. Traditionally, this was simply resolved with a quick QUIT and JOIN,
// however CHGHOST resolves this in a much cleaner fashion.
func handleCHGHOST(c *Client, e Event) {
	if len(e.Params) != 2 {
		return
	}

	c.state.Lock()
	user := c.state.lookupUser(e.Source.Name)
	if user != nil {
		user.Ident = e.Params[0]
		user.Host = e.Params[1]
	}
	c.state.Unlock()
	c.state.notify(c, UPDATE_STATE)
}

// handleAWAY handles incoming IRCv3 AWAY events, for which are sent both
// when users are no longer away, or when they are away.
func handleAWAY(c *Client, e Event) {
	c.state.Lock()
	user := c.state.lookupUser(e.Source.Name)
	if user != nil {
		user.Extras.Away = e.Last()
	}
	c.state.Unlock()
	c.state.notify(c, UPDATE_STATE)
}

// handleACCOUNT handles incoming IRCv3 ACCOUNT events. ACCOUNT is sent when
// a user logs into an account, logs out of their account, or logs into a
// different account. The account backend is handled server-side, so this
// could be NickServ, X (undernet?), etc.
func handleACCOUNT(c *Client, e Event) {
	if len(e.Params) != 1 {
		return
	}

	account := e.Params[0]
	if account == "*" {
		account = ""
	}

	c.state.Lock()
	user := c.state.lookupUser(e.Source.Name)
	if user != nil {
		user.Extras.Account = account
	}
	c.state.Unlock()
	c.state.notify(c, UPDATE_STATE)
}