source: code/trunk/irc.go@ 661

Last change on this file since 661 was 661, checked in by contact, 4 years ago

Mark bouncer users and BouncerServ as authenticated in WHOX/WHOIS

File size: 17.2 KB
RevLine 
[98]1package soju
[20]2
3import (
4 "fmt"
[350]5 "sort"
[20]6 "strings"
[516]7 "time"
[498]8 "unicode"
9 "unicode/utf8"
[43]10
11 "gopkg.in/irc.v3"
[20]12)
13
14const (
[108]15 rpl_statsping = "246"
16 rpl_localusers = "265"
17 rpl_globalusers = "266"
[162]18 rpl_creationtime = "329"
[108]19 rpl_topicwhotime = "333"
[660]20 rpl_whospcrpl = "354"
[661]21 rpl_whoisaccount = "330"
[108]22 err_invalidcapcmd = "410"
[20]23)
24
[463]25const (
26 maxMessageLength = 512
27 maxMessageParams = 15
28)
[346]29
[350]30// The server-time layout, as defined in the IRCv3 spec.
31const serverTimeLayout = "2006-01-02T15:04:05.000Z"
32
[139]33type userModes string
[20]34
[139]35func (ms userModes) Has(c byte) bool {
[20]36 return strings.IndexByte(string(ms), c) >= 0
37}
38
[139]39func (ms *userModes) Add(c byte) {
[20]40 if !ms.Has(c) {
[139]41 *ms += userModes(c)
[20]42 }
43}
44
[139]45func (ms *userModes) Del(c byte) {
[20]46 i := strings.IndexByte(string(*ms), c)
47 if i >= 0 {
48 *ms = (*ms)[:i] + (*ms)[i+1:]
49 }
50}
51
[139]52func (ms *userModes) Apply(s string) error {
[20]53 var plusMinus byte
54 for i := 0; i < len(s); i++ {
55 switch c := s[i]; c {
56 case '+', '-':
57 plusMinus = c
58 default:
59 switch plusMinus {
60 case '+':
61 ms.Add(c)
62 case '-':
63 ms.Del(c)
64 default:
65 return fmt.Errorf("malformed modestring %q: missing plus/minus", s)
66 }
67 }
68 }
69 return nil
70}
71
[139]72type channelModeType byte
73
74// standard channel mode types, as explained in https://modern.ircdocs.horse/#mode-message
75const (
76 // modes that add or remove an address to or from a list
77 modeTypeA channelModeType = iota
78 // modes that change a setting on a channel, and must always have a parameter
79 modeTypeB
80 // modes that change a setting on a channel, and must have a parameter when being set, and no parameter when being unset
81 modeTypeC
82 // modes that change a setting on a channel, and must not have a parameter
83 modeTypeD
84)
85
86var stdChannelModes = map[byte]channelModeType{
87 'b': modeTypeA, // ban list
88 'e': modeTypeA, // ban exception list
89 'I': modeTypeA, // invite exception list
90 'k': modeTypeB, // channel key
91 'l': modeTypeC, // channel user limit
92 'i': modeTypeD, // channel is invite-only
93 'm': modeTypeD, // channel is moderated
94 'n': modeTypeD, // channel has no external messages
95 's': modeTypeD, // channel is secret
96 't': modeTypeD, // channel has protected topic
97}
98
99type channelModes map[byte]string
100
[293]101// applyChannelModes parses a mode string and mode arguments from a MODE message,
102// and applies the corresponding channel mode and user membership changes on that channel.
103//
104// If ch.modes is nil, channel modes are not updated.
105//
106// needMarshaling is a list of indexes of mode arguments that represent entities
107// that must be marshaled when sent downstream.
108func applyChannelModes(ch *upstreamChannel, modeStr string, arguments []string) (needMarshaling map[int]struct{}, err error) {
109 needMarshaling = make(map[int]struct{}, len(arguments))
[139]110 nextArgument := 0
111 var plusMinus byte
[293]112outer:
[139]113 for i := 0; i < len(modeStr); i++ {
114 mode := modeStr[i]
115 if mode == '+' || mode == '-' {
116 plusMinus = mode
117 continue
118 }
119 if plusMinus != '+' && plusMinus != '-' {
[293]120 return nil, fmt.Errorf("malformed modestring %q: missing plus/minus", modeStr)
[139]121 }
122
[293]123 for _, membership := range ch.conn.availableMemberships {
124 if membership.Mode == mode {
125 if nextArgument >= len(arguments) {
126 return nil, fmt.Errorf("malformed modestring %q: missing mode argument for %c%c", modeStr, plusMinus, mode)
127 }
128 member := arguments[nextArgument]
[478]129 m := ch.Members.Value(member)
130 if m != nil {
[293]131 if plusMinus == '+' {
[478]132 m.Add(ch.conn.availableMemberships, membership)
[293]133 } else {
134 // TODO: for upstreams without multi-prefix, query the user modes again
[478]135 m.Remove(membership)
[293]136 }
137 }
138 needMarshaling[nextArgument] = struct{}{}
139 nextArgument++
140 continue outer
141 }
142 }
143
144 mt, ok := ch.conn.availableChannelModes[mode]
[139]145 if !ok {
146 continue
147 }
148 if mt == modeTypeB || (mt == modeTypeC && plusMinus == '+') {
149 if plusMinus == '+' {
150 var argument string
151 // some sentitive arguments (such as channel keys) can be omitted for privacy
152 // (this will only happen for RPL_CHANNELMODEIS, never for MODE messages)
153 if nextArgument < len(arguments) {
154 argument = arguments[nextArgument]
155 }
[293]156 if ch.modes != nil {
157 ch.modes[mode] = argument
158 }
[139]159 } else {
[293]160 delete(ch.modes, mode)
[139]161 }
162 nextArgument++
163 } else if mt == modeTypeC || mt == modeTypeD {
164 if plusMinus == '+' {
[293]165 if ch.modes != nil {
166 ch.modes[mode] = ""
167 }
[139]168 } else {
[293]169 delete(ch.modes, mode)
[139]170 }
171 }
172 }
[293]173 return needMarshaling, nil
[139]174}
175
176func (cm channelModes) Format() (modeString string, parameters []string) {
177 var modesWithValues strings.Builder
178 var modesWithoutValues strings.Builder
179 parameters = make([]string, 0, 16)
180 for mode, value := range cm {
181 if value != "" {
182 modesWithValues.WriteString(string(mode))
183 parameters = append(parameters, value)
184 } else {
185 modesWithoutValues.WriteString(string(mode))
186 }
187 }
188 modeString = "+" + modesWithValues.String() + modesWithoutValues.String()
189 return
190}
191
192const stdChannelTypes = "#&+!"
193
[20]194type channelStatus byte
195
196const (
197 channelPublic channelStatus = '='
198 channelSecret channelStatus = '@'
199 channelPrivate channelStatus = '*'
200)
201
202func parseChannelStatus(s string) (channelStatus, error) {
203 if len(s) > 1 {
204 return 0, fmt.Errorf("invalid channel status %q: more than one character", s)
205 }
206 switch cs := channelStatus(s[0]); cs {
207 case channelPublic, channelSecret, channelPrivate:
208 return cs, nil
209 default:
210 return 0, fmt.Errorf("invalid channel status %q: unknown status", s)
211 }
212}
213
[139]214type membership struct {
215 Mode byte
216 Prefix byte
217}
[20]218
[139]219var stdMemberships = []membership{
220 {'q', '~'}, // founder
221 {'a', '&'}, // protected
222 {'o', '@'}, // operator
223 {'h', '%'}, // halfop
224 {'v', '+'}, // voice
225}
[20]226
[292]227// memberships always sorted by descending membership rank
228type memberships []membership
229
230func (m *memberships) Add(availableMemberships []membership, newMembership membership) {
231 l := *m
232 i := 0
233 for _, availableMembership := range availableMemberships {
234 if i >= len(l) {
235 break
236 }
237 if l[i] == availableMembership {
238 if availableMembership == newMembership {
239 // we already have this membership
240 return
241 }
242 i++
243 continue
244 }
245 if availableMembership == newMembership {
246 break
247 }
[128]248 }
[292]249 // insert newMembership at i
250 l = append(l, membership{})
251 copy(l[i+1:], l[i:])
252 l[i] = newMembership
253 *m = l
[128]254}
255
[292]256func (m *memberships) Remove(oldMembership membership) {
257 l := *m
258 for i, currentMembership := range l {
259 if currentMembership == oldMembership {
260 *m = append(l[:i], l[i+1:]...)
261 return
262 }
263 }
264}
265
266func (m memberships) Format(dc *downstreamConn) string {
267 if !dc.caps["multi-prefix"] {
268 if len(m) == 0 {
269 return ""
270 }
271 return string(m[0].Prefix)
272 }
273 prefixes := make([]byte, len(m))
274 for i, membership := range m {
275 prefixes[i] = membership.Prefix
276 }
277 return string(prefixes)
278}
279
[43]280func parseMessageParams(msg *irc.Message, out ...*string) error {
281 if len(msg.Params) < len(out) {
282 return newNeedMoreParamsError(msg.Command)
283 }
284 for i := range out {
285 if out[i] != nil {
286 *out[i] = msg.Params[i]
287 }
288 }
289 return nil
290}
[153]291
[303]292func copyClientTags(tags irc.Tags) irc.Tags {
293 t := make(irc.Tags, len(tags))
294 for k, v := range tags {
295 if strings.HasPrefix(k, "+") {
296 t[k] = v
297 }
298 }
299 return t
300}
301
[153]302type batch struct {
303 Type string
304 Params []string
305 Outer *batch // if not-nil, this batch is nested in Outer
[155]306 Label string
[153]307}
[193]308
[350]309func join(channels, keys []string) []*irc.Message {
310 // Put channels with a key first
311 js := joinSorter{channels, keys}
312 sort.Sort(&js)
313
314 // Two spaces because there are three words (JOIN, channels and keys)
315 maxLength := maxMessageLength - (len("JOIN") + 2)
316
317 var msgs []*irc.Message
318 var channelsBuf, keysBuf strings.Builder
319 for i, channel := range channels {
320 key := keys[i]
321
322 n := channelsBuf.Len() + keysBuf.Len() + 1 + len(channel)
323 if key != "" {
324 n += 1 + len(key)
325 }
326
327 if channelsBuf.Len() > 0 && n > maxLength {
328 // No room for the new channel in this message
329 params := []string{channelsBuf.String()}
330 if keysBuf.Len() > 0 {
331 params = append(params, keysBuf.String())
332 }
333 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
334 channelsBuf.Reset()
335 keysBuf.Reset()
336 }
337
338 if channelsBuf.Len() > 0 {
339 channelsBuf.WriteByte(',')
340 }
341 channelsBuf.WriteString(channel)
342 if key != "" {
343 if keysBuf.Len() > 0 {
344 keysBuf.WriteByte(',')
345 }
346 keysBuf.WriteString(key)
347 }
348 }
349 if channelsBuf.Len() > 0 {
350 params := []string{channelsBuf.String()}
351 if keysBuf.Len() > 0 {
352 params = append(params, keysBuf.String())
353 }
354 msgs = append(msgs, &irc.Message{Command: "JOIN", Params: params})
355 }
356
357 return msgs
358}
359
[463]360func generateIsupport(prefix *irc.Prefix, nick string, tokens []string) []*irc.Message {
361 maxTokens := maxMessageParams - 2 // 2 reserved params: nick + text
362
363 var msgs []*irc.Message
364 for len(tokens) > 0 {
365 var msgTokens []string
366 if len(tokens) > maxTokens {
367 msgTokens = tokens[:maxTokens]
368 tokens = tokens[maxTokens:]
369 } else {
370 msgTokens = tokens
371 tokens = nil
372 }
373
374 msgs = append(msgs, &irc.Message{
375 Prefix: prefix,
376 Command: irc.RPL_ISUPPORT,
377 Params: append(append([]string{nick}, msgTokens...), "are supported"),
378 })
379 }
380
381 return msgs
382}
383
[636]384func generateMOTD(prefix *irc.Prefix, nick string, motd string) []*irc.Message {
385 var msgs []*irc.Message
386 msgs = append(msgs, &irc.Message{
387 Prefix: prefix,
388 Command: irc.RPL_MOTDSTART,
389 Params: []string{nick, fmt.Sprintf("- Message of the Day -")},
390 })
391
392 for _, l := range strings.Split(motd, "\n") {
393 msgs = append(msgs, &irc.Message{
394 Prefix: prefix,
395 Command: irc.RPL_MOTD,
396 Params: []string{nick, l},
397 })
398 }
399
400 msgs = append(msgs, &irc.Message{
401 Prefix: prefix,
402 Command: irc.RPL_ENDOFMOTD,
403 Params: []string{nick, "End of /MOTD command."},
404 })
405
406 return msgs
407}
408
[350]409type joinSorter struct {
410 channels []string
411 keys []string
412}
413
414func (js *joinSorter) Len() int {
415 return len(js.channels)
416}
417
418func (js *joinSorter) Less(i, j int) bool {
419 if (js.keys[i] != "") != (js.keys[j] != "") {
420 // Only one of the channels has a key
421 return js.keys[i] != ""
422 }
423 return js.channels[i] < js.channels[j]
424}
425
426func (js *joinSorter) Swap(i, j int) {
427 js.channels[i], js.channels[j] = js.channels[j], js.channels[i]
428 js.keys[i], js.keys[j] = js.keys[j], js.keys[i]
429}
[392]430
431// parseCTCPMessage parses a CTCP message. CTCP is defined in
432// https://tools.ietf.org/html/draft-oakley-irc-ctcp-02
433func parseCTCPMessage(msg *irc.Message) (cmd string, params string, ok bool) {
434 if (msg.Command != "PRIVMSG" && msg.Command != "NOTICE") || len(msg.Params) < 2 {
435 return "", "", false
436 }
437 text := msg.Params[1]
438
439 if !strings.HasPrefix(text, "\x01") {
440 return "", "", false
441 }
442 text = strings.Trim(text, "\x01")
443
444 words := strings.SplitN(text, " ", 2)
445 cmd = strings.ToUpper(words[0])
446 if len(words) > 1 {
447 params = words[1]
448 }
449
450 return cmd, params, true
451}
[478]452
453type casemapping func(string) string
454
455func casemapNone(name string) string {
456 return name
457}
458
459// CasemapASCII of name is the canonical representation of name according to the
460// ascii casemapping.
461func casemapASCII(name string) string {
[492]462 nameBytes := []byte(name)
463 for i, r := range nameBytes {
[478]464 if 'A' <= r && r <= 'Z' {
[492]465 nameBytes[i] = r + 'a' - 'A'
[478]466 }
467 }
[492]468 return string(nameBytes)
[478]469}
470
471// casemapRFC1459 of name is the canonical representation of name according to the
472// rfc1459 casemapping.
473func casemapRFC1459(name string) string {
[492]474 nameBytes := []byte(name)
475 for i, r := range nameBytes {
[478]476 if 'A' <= r && r <= 'Z' {
[492]477 nameBytes[i] = r + 'a' - 'A'
[478]478 } else if r == '{' {
[492]479 nameBytes[i] = '['
[478]480 } else if r == '}' {
[492]481 nameBytes[i] = ']'
[478]482 } else if r == '\\' {
[492]483 nameBytes[i] = '|'
[478]484 } else if r == '~' {
[492]485 nameBytes[i] = '^'
[478]486 }
487 }
[492]488 return string(nameBytes)
[478]489}
490
491// casemapRFC1459Strict of name is the canonical representation of name
492// according to the rfc1459-strict casemapping.
493func casemapRFC1459Strict(name string) string {
[492]494 nameBytes := []byte(name)
495 for i, r := range nameBytes {
[478]496 if 'A' <= r && r <= 'Z' {
[492]497 nameBytes[i] = r + 'a' - 'A'
[478]498 } else if r == '{' {
[492]499 nameBytes[i] = '['
[478]500 } else if r == '}' {
[492]501 nameBytes[i] = ']'
[478]502 } else if r == '\\' {
[492]503 nameBytes[i] = '|'
[478]504 }
505 }
[492]506 return string(nameBytes)
[478]507}
508
509func parseCasemappingToken(tokenValue string) (casemap casemapping, ok bool) {
510 switch tokenValue {
511 case "ascii":
512 casemap = casemapASCII
513 case "rfc1459":
514 casemap = casemapRFC1459
515 case "rfc1459-strict":
516 casemap = casemapRFC1459Strict
517 default:
518 return nil, false
519 }
520 return casemap, true
521}
522
523func partialCasemap(higher casemapping, name string) string {
[492]524 nameFullyCM := []byte(higher(name))
525 nameBytes := []byte(name)
526 for i, r := range nameBytes {
527 if !('A' <= r && r <= 'Z') && !('a' <= r && r <= 'z') {
528 nameBytes[i] = nameFullyCM[i]
[478]529 }
530 }
[492]531 return string(nameBytes)
[478]532}
533
534type casemapMap struct {
535 innerMap map[string]casemapEntry
536 casemap casemapping
537}
538
539type casemapEntry struct {
540 originalKey string
541 value interface{}
542}
543
544func newCasemapMap(size int) casemapMap {
545 return casemapMap{
546 innerMap: make(map[string]casemapEntry, size),
547 casemap: casemapNone,
548 }
549}
550
551func (cm *casemapMap) OriginalKey(name string) (key string, ok bool) {
552 entry, ok := cm.innerMap[cm.casemap(name)]
553 if !ok {
554 return "", false
555 }
556 return entry.originalKey, true
557}
558
559func (cm *casemapMap) Has(name string) bool {
560 _, ok := cm.innerMap[cm.casemap(name)]
561 return ok
562}
563
564func (cm *casemapMap) Len() int {
565 return len(cm.innerMap)
566}
567
568func (cm *casemapMap) SetValue(name string, value interface{}) {
569 nameCM := cm.casemap(name)
570 entry, ok := cm.innerMap[nameCM]
571 if !ok {
572 cm.innerMap[nameCM] = casemapEntry{
573 originalKey: name,
574 value: value,
575 }
576 return
577 }
578 entry.value = value
579 cm.innerMap[nameCM] = entry
580}
581
582func (cm *casemapMap) Delete(name string) {
583 delete(cm.innerMap, cm.casemap(name))
584}
585
586func (cm *casemapMap) SetCasemapping(newCasemap casemapping) {
587 cm.casemap = newCasemap
588 newInnerMap := make(map[string]casemapEntry, len(cm.innerMap))
589 for _, entry := range cm.innerMap {
590 newInnerMap[cm.casemap(entry.originalKey)] = entry
591 }
592 cm.innerMap = newInnerMap
593}
594
595type upstreamChannelCasemapMap struct{ casemapMap }
596
597func (cm *upstreamChannelCasemapMap) Value(name string) *upstreamChannel {
598 entry, ok := cm.innerMap[cm.casemap(name)]
599 if !ok {
600 return nil
601 }
602 return entry.value.(*upstreamChannel)
603}
604
605type channelCasemapMap struct{ casemapMap }
606
607func (cm *channelCasemapMap) Value(name string) *Channel {
608 entry, ok := cm.innerMap[cm.casemap(name)]
609 if !ok {
610 return nil
611 }
612 return entry.value.(*Channel)
613}
614
615type membershipsCasemapMap struct{ casemapMap }
616
617func (cm *membershipsCasemapMap) Value(name string) *memberships {
618 entry, ok := cm.innerMap[cm.casemap(name)]
619 if !ok {
620 return nil
621 }
622 return entry.value.(*memberships)
623}
624
[480]625type deliveredCasemapMap struct{ casemapMap }
[478]626
[480]627func (cm *deliveredCasemapMap) Value(name string) deliveredClientMap {
[478]628 entry, ok := cm.innerMap[cm.casemap(name)]
629 if !ok {
630 return nil
631 }
[480]632 return entry.value.(deliveredClientMap)
[478]633}
[498]634
635func isWordBoundary(r rune) bool {
636 switch r {
637 case '-', '_', '|':
638 return false
639 case '\u00A0':
640 return true
641 default:
642 return !unicode.IsLetter(r) && !unicode.IsNumber(r)
643 }
644}
645
646func isHighlight(text, nick string) bool {
647 for {
648 i := strings.Index(text, nick)
649 if i < 0 {
650 return false
651 }
652
653 // Detect word boundaries
654 var left, right rune
655 if i > 0 {
656 left, _ = utf8.DecodeLastRuneInString(text[:i])
657 }
658 if i < len(text) {
659 right, _ = utf8.DecodeRuneInString(text[i+len(nick):])
660 }
661 if isWordBoundary(left) && isWordBoundary(right) {
662 return true
663 }
664
665 text = text[i+len(nick):]
666 }
667}
[516]668
669// parseChatHistoryBound parses the given CHATHISTORY parameter as a bound.
670// The zero time is returned on error.
671func parseChatHistoryBound(param string) time.Time {
672 parts := strings.SplitN(param, "=", 2)
673 if len(parts) != 2 {
674 return time.Time{}
675 }
676 switch parts[0] {
677 case "timestamp":
678 timestamp, err := time.Parse(serverTimeLayout, parts[1])
679 if err != nil {
680 return time.Time{}
681 }
682 return timestamp
683 default:
684 return time.Time{}
685 }
686}
[660]687
688type whoxInfo struct {
689 Token string
690 Username string
691 Hostname string
692 Server string
693 Nickname string
694 Flags string
695 Account string
696 Realname string
697}
698
699func generateWHOXReply(prefix *irc.Prefix, nick, fields string, info *whoxInfo) *irc.Message {
700 if fields == "" {
701 return &irc.Message{
702 Prefix: prefix,
703 Command: irc.RPL_WHOREPLY,
704 Params: []string{nick, "*", info.Username, info.Hostname, info.Server, info.Nickname, info.Flags, "0 " + info.Realname},
705 }
706 }
707
708 fieldSet := make(map[byte]bool)
709 for i := 0; i < len(fields); i++ {
710 fieldSet[fields[i]] = true
711 }
712
713 var params []string
714 if fieldSet['t'] {
715 params = append(params, info.Token)
716 }
717 if fieldSet['c'] {
718 params = append(params, "*")
719 }
720 if fieldSet['u'] {
721 params = append(params, info.Username)
722 }
723 if fieldSet['i'] {
724 params = append(params, "255.255.255.255")
725 }
726 if fieldSet['h'] {
727 params = append(params, info.Hostname)
728 }
729 if fieldSet['s'] {
730 params = append(params, info.Server)
731 }
732 if fieldSet['n'] {
733 params = append(params, info.Nickname)
734 }
735 if fieldSet['f'] {
736 params = append(params, info.Flags)
737 }
738 if fieldSet['d'] {
739 params = append(params, "0")
740 }
741 if fieldSet['l'] { // idle time
742 params = append(params, "0")
743 }
744 if fieldSet['a'] {
745 account := "0" // WHOX uses "0" to mean "no account"
746 if info.Account != "" && info.Account != "*" {
747 account = info.Account
748 }
749 params = append(params, account)
750 }
751 if fieldSet['o'] {
752 params = append(params, "0")
753 }
754 if fieldSet['r'] {
755 params = append(params, info.Realname)
756 }
757
758 return &irc.Message{
759 Prefix: prefix,
760 Command: rpl_whospcrpl,
761 Params: append([]string{nick}, params...),
762 }
763}
Note: See TracBrowser for help on using the repository browser.