summaryrefslogtreecommitdiff
path: root/deprecated-webircgateway/pkg/webircgateway/client_command_handlers.go
blob: d5d1fccbeab705d9e9ca9a7dd895ce0d44312282 (plain)
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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
package webircgateway

import (
	"errors"
	"strconv"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v4"
	"github.com/kiwiirc/webircgateway/pkg/irc"
	"github.com/kiwiirc/webircgateway/pkg/recaptcha"
	"golang.org/x/net/html/charset"
	"golang.org/x/time/rate"
)

var MAX_EXTJWT_SIZE = 200

/*
 * ProcessLineFromUpstream
 * Processes and makes any changes to a line of data sent from an upstream
 */
func (c *Client) ProcessLineFromUpstream(data string) string {
	client := c

	m, parseErr := irc.ParseLine(data)
	if parseErr != nil {
		return data
	}

	pLen := len(m.Params)

	if pLen > 0 && m.Command == "NICK" && m.Prefix.Nick == c.IrcState.Nick {
		client.IrcState.Nick = m.Params[0]
	}
	if pLen > 0 && m.Command == "001" {
		client.IrcState.Nick = m.Params[0]
		client.State = ClientStateConnected
		client.ServerMessagePrefix = *m.Prefix

		// Throttle writes if configured, but only after registration is complete. Typical IRCd
		// behavior is to not throttle registration commands.
		client.ThrottledRecv.Limiter = rate.NewLimiter(rate.Limit(client.UpstreamConfig.Throttle), 1)
	}
	if pLen > 0 && m.Command == "005" {
		tokenPairs := m.Params[1 : pLen-1]
		iSupport := c.IrcState.ISupport
		iSupport.Received = true
		iSupport.Tags = m.Tags
		iSupport.AddTokens(tokenPairs)
	}
	if c.IrcState.ISupport.Received && !c.IrcState.ISupport.Injected && m.Command != "005" {
		iSupport := c.IrcState.ISupport
		iSupport.Injected = true

		msg := irc.NewMessage()
		msg.Command = "005"
		msg.Prefix = &c.ServerMessagePrefix
		msg.Params = append(msg.Params, c.IrcState.Nick)

		if iSupport.HasToken("EXTJWT") {
			c.Log(1, "Upstream already supports EXTJWT, disabling feature")
			c.Features.ExtJwt = false
		} else {
			// Add EXTJWT ISupport token
			msg.Params = append(msg.Params, "EXTJWT=1")
			iSupport.AddToken("EXTJWT=1")
		}

		msg.Params = append(msg.Params, "are supported by this server")
		if timeTag, ok := c.IrcState.ISupport.Tags["time"]; ok {
			msg.Tags["time"] = timeTag
		}
		if len(msg.Params) > 2 {
			// Extra tokens were added, send the line
			c.SendClientSignal("data", msg.ToLine())
		}
	}
	if pLen > 0 && m.Command == "JOIN" && m.Prefix.Nick == c.IrcState.Nick {
		channel := irc.NewStateChannel(m.GetParam(0, ""))
		c.IrcState.SetChannel(channel)
	}
	if pLen > 0 && m.Command == "PART" && m.Prefix.Nick == c.IrcState.Nick {
		c.IrcState.RemoveChannel(m.GetParam(0, ""))
	}
	if pLen > 0 && m.Command == "QUIT" && m.Prefix.Nick == c.IrcState.Nick {
		c.IrcState.ClearChannels()
	}
	// :server.com 900 m m!m@irc-3jg.1ab.j4ep8h.IP prawnsalad :You are now logged in as prawnsalad
	if pLen > 0 && m.Command == "900" {
		c.IrcState.Account = m.GetParam(2, "")
	}
	// :server.com 901 itsonlybinary itsonlybinary!itsonlybina@user/itsonlybinary :You are now logged out
	if m.Command == "901" {
		c.IrcState.Account = ""
	}
	// :prawnsalad!prawn@kiwiirc/prawnsalad MODE #kiwiirc-dev +oo notprawn kiwi-n75
	if pLen > 0 && m.Command == "MODE" {
		if strings.HasPrefix(m.GetParam(0, ""), "#") {
			channelName := m.GetParam(0, "")
			modes := m.GetParam(1, "")

			channel := c.IrcState.GetChannel(channelName)
			if channel != nil {
				channel = irc.NewStateChannel(channelName)
				c.IrcState.SetChannel(channel)
			}

			adding := false
			paramIdx := 1
			for i := 0; i < len(modes); i++ {
				mode := string(modes[i])

				if mode == "+" {
					adding = true
				} else if mode == "-" {
					adding = false
				} else {
					paramIdx++
					param := m.GetParam(paramIdx, "")
					if strings.EqualFold(param, c.IrcState.Nick) {
						if adding {
							channel.Modes[mode] = ""
						} else {
							delete(channel.Modes, mode)
						}
					}
				}
			}
		}
	}

	// If upstream reports that it supports message-tags natively, disable the wrapping of this feature for
	// this client
	if pLen >= 3 &&
		strings.ToUpper(m.Command) == "CAP" &&
		m.GetParamU(1, "") == "LS" {
		// The CAPs could be param 2 or 3 depending on if were using multiple lines to list them all.
		caps := ""
		if pLen >= 4 && m.Params[2] == "*" {
			caps = m.GetParamU(3, "")
		} else {
			caps = m.GetParamU(2, "")
		}

		if containsOneOf(caps, []string{"DRAFT/MESSAGE-TAGS-0.2", "MESSAGE-TAGS"}) {
			c.Log(1, "Upstream already supports Messagetags, disabling feature")
			c.Features.Messagetags = false
		}

		// Inject message-tags cap into the last line of IRCd capabilities
		if c.Features.Messagetags && m.Params[2] != "*" {
			m.Params[2] += " message-tags"
			data = m.ToLine()
		}
	}

	// If we requested message-tags, make sure to include it in the ACK when
	// the IRCd sends the ACK through
	if m != nil &&
		client.RequestedMessageTagsCap != "" &&
		strings.ToUpper(m.Command) == "CAP" &&
		m.GetParamU(1, "") == "ACK" &&
		!strings.Contains(m.GetParamU(2, ""), "MESSAGE-TAGS") {

		m.Params[2] += " " + client.RequestedMessageTagsCap
		data = m.ToLine()

		client.RequestedMessageTagsCap = ""
	}

	if m != nil && client.Features.Messagetags && c.Gateway.messageTags.CanMessageContainClientTags(m) {
		// If we have any message tags stored for this message from a previous PRIVMSG sent
		// by a client, add them back in
		mTags, mTagsExists := c.Gateway.messageTags.GetTagsFromMessage(client, m.Prefix.Nick, m)
		if mTagsExists {
			for k, v := range mTags.Tags {
				m.Tags[k] = v
			}

			data = m.ToLine()
		}
	}

	return data
}

/*
 * ProcessLineFromClient
 * Processes and makes any changes to a line of data sent from a client
 */
func (c *Client) ProcessLineFromClient(line string) (string, error) {
	message, err := irc.ParseLine(line)
	// Just pass any random data upstream
	if err != nil {
		return line, nil
	}

	maybeConnectUpstream := func() {
		verified := false
		if c.RequiresVerification && !c.Verified {
			verified = false
		} else {
			verified = true
		}

		if !c.UpstreamStarted && c.IrcState.Username != "" && c.IrcState.Nick != "" && verified {
			c.connectUpstream()
		}
	}

	if !c.Verified && strings.ToUpper(message.Command) == "CAPTCHA" {
		verified := false
		if len(message.Params) >= 1 {
			captcha := recaptcha.R{
				URL:    c.Gateway.Config.ReCaptchaURL,
				Secret: c.Gateway.Config.ReCaptchaSecret,
			}

			verified = captcha.VerifyResponse(message.Params[0])
		}

		if !verified {
			c.SendIrcError("Invalid captcha")
			c.SendClientSignal("state", "closed", "bad_captcha")
			c.StartShutdown("unverifed")
		} else {
			c.Verified = true
			maybeConnectUpstream()
		}

		return "", nil
	}

	// NICK <nickname>
	if strings.ToUpper(message.Command) == "NICK" && !c.UpstreamStarted {
		if len(message.Params) > 0 {
			c.IrcState.Nick = message.Params[0]
		}

		if !c.UpstreamStarted {
			maybeConnectUpstream()
		}
	}

	// USER <username> <hostname> <servername> <realname>
	if strings.ToUpper(message.Command) == "USER" && !c.UpstreamStarted {
		if len(message.Params) < 4 {
			return line, errors.New("Invalid USER line")
		}

		if c.Gateway.Config.ClientUsername != "" {
			message.Params[0] = makeClientReplacements(c.Gateway.Config.ClientUsername, c)
		}
		if c.Gateway.Config.ClientRealname != "" {
			message.Params[3] = makeClientReplacements(c.Gateway.Config.ClientRealname, c)
		}

		line = message.ToLine()

		c.IrcState.Username = message.Params[0]
		c.IrcState.RealName = message.Params[3]

		maybeConnectUpstream()
	}

	if strings.ToUpper(message.Command) == "ENCODING" {
		if len(message.Params) > 0 {
			encoding, _ := charset.Lookup(message.Params[0])
			if encoding == nil {
				c.Log(1, "Requested unknown encoding, %s", message.Params[0])
			} else {
				c.Encoding = message.Params[0]
				c.Log(1, "Set encoding to %s", message.Params[0])
			}
		}

		// Don't send the ENCODING command upstream
		return "", nil
	}

	if strings.ToUpper(message.Command) == "HOST" && !c.UpstreamStarted {
		// HOST irc.network.net:6667
		// HOST irc.network.net:+6667

		if !c.Gateway.Config.Gateway {
			return "", nil
		}

		if len(message.Params) == 0 {
			return "", nil
		}

		addr := message.Params[0]
		if addr == "" {
			c.SendIrcError("Missing host")
			c.StartShutdown("missing_host")
			return "", nil
		}

		// Parse host:+port into the c.dest* vars
		portSep := strings.LastIndex(addr, ":")
		if portSep == -1 {
			c.DestHost = addr
			c.DestPort = 6667
			c.DestTLS = false
		} else {
			c.DestHost = addr[0:portSep]
			portParam := addr[portSep+1:]
			if len(portParam) > 0 && portParam[0:1] == "+" {
				c.DestTLS = true
				c.DestPort, err = strconv.Atoi(portParam[1:])
				if err != nil {
					c.DestPort = 6697
				}
			} else {
				c.DestPort, err = strconv.Atoi(portParam[0:])
				if err != nil {
					c.DestPort = 6667
				}
			}
		}

		// Don't send the HOST command upstream
		return "", nil
	}

	// If the client supports CAP, assume the client also supports parsing MessageTags
	// When upstream replies with its CAP listing, we check if message-tags is supported by the IRCd already and if so,
	// we disable this feature flag again to use the IRCds native support.
	if strings.ToUpper(message.Command) == "CAP" && len(message.Params) > 0 && strings.ToUpper(message.Params[0]) == "LS" {
		c.Log(1, "Enabling client Messagetags feature")
		c.Features.Messagetags = true
	}

	// If we are wrapping the Messagetags feature, make sure the clients REQ message-tags doesn't
	// get sent upstream
	if c.Features.Messagetags && strings.ToUpper(message.Command) == "CAP" && message.GetParamU(0, "") == "REQ" {
		reqCaps := strings.ToLower(message.GetParam(1, ""))
		capsThatEnableMessageTags := []string{"message-tags", "account-tag", "server-time", "batch"}

		if strings.Contains(reqCaps, "message-tags") {
			// Rebuild the list of requested caps, without message-tags
			caps := strings.Split(reqCaps, " ")
			newCaps := []string{}
			for _, cap := range caps {
				if !strings.Contains(strings.ToLower(cap), "message-tags") {
					newCaps = append(newCaps, cap)
				} else {
					c.RequestedMessageTagsCap = cap
				}
			}

			if len(newCaps) == 0 {
				// The only requested CAP was our emulated message-tags
				// the server will not be sending an ACK so we need to send our own
				c.SendClientSignal("data", "CAP * ACK :"+c.RequestedMessageTagsCap)
				return "", nil
			}
			message.Params[1] = strings.Join(newCaps, " ")
			line = message.ToLine()
		} else if !containsOneOf(reqCaps, capsThatEnableMessageTags) {
			// Didn't request anything that needs message-tags cap so disable it
			c.Features.Messagetags = false
		}
	}

	if c.Features.Messagetags && message.Command == "TAGMSG" {
		if len(message.Params) == 0 {
			return "", nil
		}

		// We can't be 100% sure what this users correct mask is, so just send the nick
		message.Prefix.Nick = c.IrcState.Nick
		message.Prefix.Hostname = ""
		message.Prefix.Username = ""

		thisHost := strings.ToLower(c.UpstreamConfig.Hostname)
		target := message.Params[0]
		for val := range c.Gateway.Clients.IterBuffered() {
			curClient := val.Val.(*Client)
			sameHost := strings.ToLower(curClient.UpstreamConfig.Hostname) == thisHost
			if !sameHost {
				continue
			}

			// Only send the message on to either the target nick, or the clients in a set channel
			if !strings.EqualFold(target, curClient.IrcState.Nick) && !curClient.IrcState.HasChannel(target) {
				continue
			}

			curClient.SendClientSignal("data", message.ToLine())
		}

		return "", nil
	}

	// Check for any client message tags so that we can store them for replaying to other clients
	if c.Features.Messagetags && c.Gateway.messageTags.CanMessageContainClientTags(message) {
		c.Gateway.messageTags.AddTagsFromMessage(c, c.IrcState.Nick, message)
		// Prevent any client tags heading upstream
		for k := range message.Tags {
			if len(k) > 0 && k[0] == '+' {
				delete(message.Tags, k)
			}
		}

		line = message.ToLine()
	}

	if c.Features.ExtJwt && strings.ToUpper(message.Command) == "EXTJWT" {
		tokenTarget := message.GetParam(0, "")
		tokenService := message.GetParam(1, "")

		tokenM := irc.Message{}
		tokenM.Command = "EXTJWT"
		tokenM.Prefix = &c.ServerMessagePrefix
		tokenData := jwt.MapClaims{
			"exp":     time.Now().UTC().Add(1 * time.Minute).Unix(),
			"iss":     c.UpstreamConfig.Hostname,
			"sub":     c.IrcState.Nick,
			"account": c.IrcState.Account,
			"umodes":  []string{},

			// Channel specific claims
			"channel": "",
			"joined":  0,
			"cmodes":  []string{},
		}

		// Use the NetworkCommonAddress if a plugin as assigned one.
		// This allows plugins to associate different upstream hosts to the same network
		if c.UpstreamConfig.NetworkCommonAddress != "" {
			tokenData["iss"] = c.UpstreamConfig.NetworkCommonAddress
		}

		if tokenTarget == "" || tokenTarget == "*" {
			tokenM.Params = append(tokenM.Params, "*")
		} else {
			targetChan := c.IrcState.GetChannel(tokenTarget)
			if targetChan == nil {
				// Channel does not exist in IRC State, send so such channel message
				failMessage := irc.Message{
					Command: "403", // ERR_NOSUCHCHANNEL
					Prefix:  &c.ServerMessagePrefix,
					Params:  []string{c.IrcState.Nick, tokenTarget, "No such channel"},
				}
				c.SendClientSignal("data", failMessage.ToLine())
				return "", nil
			}

			tokenM.Params = append(tokenM.Params, tokenTarget)

			tokenData["channel"] = targetChan.Name
			tokenData["joined"] = targetChan.Joined.Unix()

			modes := []string{}
			for mode := range targetChan.Modes {
				modes = append(modes, mode)
			}
			tokenData["cmodes"] = modes
		}

		if tokenService == "" || tokenService == "*" {
			tokenM.Params = append(tokenM.Params, "*")
		} else {
			c.SendIrcFail("EXTJWT", "NO_SUCH_SERVICE", "No such service")
			return "", nil
		}

		token := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData)
		tokenSigned, tokenSignedErr := token.SignedString([]byte(c.Gateway.Config.Secret))
		if tokenSignedErr != nil {
			c.Log(3, "Error creating JWT token. %s", tokenSignedErr.Error())
			c.SendIrcFail("EXTJWT", "UNKNOWN_ERROR", "Failed to generate token")
			return "", nil
		}

		// Spit token if it exceeds max length
		for len(tokenSigned) > MAX_EXTJWT_SIZE {
			tokenSignedPart := tokenSigned[:MAX_EXTJWT_SIZE]
			tokenSigned = tokenSigned[MAX_EXTJWT_SIZE:]

			tokenPartM := tokenM
			tokenPartM.Params = append(tokenPartM.Params, "*", tokenSignedPart)
			c.SendClientSignal("data", tokenPartM.ToLine())
		}

		tokenM.Params = append(tokenM.Params, tokenSigned)
		c.SendClientSignal("data", tokenM.ToLine())

		return "", nil
	}

	return line, nil
}