source: code/trunk/upstream.go@ 278

Last change on this file since 278 was 278, checked in by contact, 5 years ago

Add upstreamConn.caps

Instead of adding one field per capability, let's just have a map, just
like downstreamConn.

File size: 35.0 KB
RevLine 
[98]1package soju
[13]2
3import (
4 "crypto/tls"
[95]5 "encoding/base64"
[155]6 "errors"
[13]7 "fmt"
8 "io"
9 "net"
[19]10 "strconv"
[17]11 "strings"
[19]12 "time"
[13]13
[95]14 "github.com/emersion/go-sasl"
[13]15 "gopkg.in/irc.v3"
16)
17
[19]18type upstreamChannel struct {
[162]19 Name string
20 conn *upstreamConn
21 Topic string
22 TopicWho string
23 TopicTime time.Time
24 Status channelStatus
25 modes channelModes
26 creationTime string
27 Members map[string]*membership
28 complete bool
[19]29}
30
[13]31type upstreamConn struct {
[210]32 conn
[16]33
[210]34 network *network
35 user *user
36
[16]37 serverName string
38 availableUserModes string
[139]39 availableChannelModes map[byte]channelModeType
40 availableChannelTypes string
41 availableMemberships []membership
[19]42
[277]43 registered bool
44 nick string
45 username string
46 realname string
47 modes userModes
48 channels map[string]*upstreamChannel
49 supportedCaps map[string]string
[278]50 caps map[string]bool
[277]51 batches map[string]batch
52 away bool
[278]53 nextLabelID uint64
[95]54
55 saslClient sasl.Client
56 saslStarted bool
[177]57
58 // set of LIST commands in progress, per downstream
59 pendingLISTDownstreamSet map[uint64]struct{}
[178]60
[215]61 messageLoggers map[string]*messageLogger
[13]62}
63
[77]64func connectToUpstream(network *network) (*upstreamConn, error) {
65 logger := &prefixLogger{network.user.srv.Logger, fmt.Sprintf("upstream %q: ", network.Addr)}
[33]66
[269]67 var scheme string
68 var addr string
69
70 addrParts := strings.SplitN(network.Addr, "://", 2)
71 if len(addrParts) == 2 {
72 scheme = addrParts[0]
73 addr = addrParts[1]
74 } else {
75 scheme = "ircs"
76 addr = addrParts[0]
[77]77 }
78
[206]79 dialer := net.Dialer{Timeout: connectTimeout}
80
[269]81 var netConn net.Conn
82 var err error
83 switch scheme {
84 case "ircs":
85 if !strings.ContainsRune(addr, ':') {
86 addr = addr + ":6697"
87 }
88
89 logger.Printf("connecting to TLS server at address %q", addr)
90 netConn, err = tls.DialWithDialer(&dialer, "tcp", addr, nil)
[270]91 case "irc+insecure":
92 if !strings.ContainsRune(addr, ':') {
93 addr = addr + ":6667"
94 }
95
96 logger.Printf("connecting to plain-text server at address %q", addr)
97 netConn, err = dialer.Dial("tcp", addr)
[269]98 default:
99 return nil, fmt.Errorf("failed to dial %q: unknown scheme: %v", addr, scheme)
100 }
[33]101 if err != nil {
[77]102 return nil, fmt.Errorf("failed to dial %q: %v", addr, err)
[33]103 }
104
[55]105 uc := &upstreamConn{
[210]106 conn: *newConn(network.user.srv, netConn, logger),
[177]107 network: network,
108 user: network.user,
109 channels: make(map[string]*upstreamChannel),
[277]110 supportedCaps: make(map[string]string),
[278]111 caps: make(map[string]bool),
[177]112 batches: make(map[string]batch),
113 availableChannelTypes: stdChannelTypes,
114 availableChannelModes: stdChannelModes,
115 availableMemberships: stdMemberships,
116 pendingLISTDownstreamSet: make(map[uint64]struct{}),
[215]117 messageLoggers: make(map[string]*messageLogger),
[33]118 }
119
[55]120 return uc, nil
[33]121}
122
[73]123func (uc *upstreamConn) forEachDownstream(f func(*downstreamConn)) {
[218]124 uc.network.forEachDownstream(f)
[73]125}
126
[161]127func (uc *upstreamConn) forEachDownstreamByID(id uint64, f func(*downstreamConn)) {
[155]128 uc.forEachDownstream(func(dc *downstreamConn) {
129 if id != 0 && id != dc.id {
130 return
131 }
132 f(dc)
133 })
134}
135
[55]136func (uc *upstreamConn) getChannel(name string) (*upstreamChannel, error) {
137 ch, ok := uc.channels[name]
[19]138 if !ok {
139 return nil, fmt.Errorf("unknown channel %q", name)
140 }
141 return ch, nil
142}
143
[129]144func (uc *upstreamConn) isChannel(entity string) bool {
[139]145 if i := strings.IndexByte(uc.availableChannelTypes, entity[0]); i >= 0 {
146 return true
[129]147 }
148 return false
149}
150
[181]151func (uc *upstreamConn) getPendingLIST() *pendingLIST {
[177]152 for _, pl := range uc.user.pendingLISTs {
153 if _, ok := pl.pendingCommands[uc.network.ID]; !ok {
154 continue
155 }
156 return &pl
157 }
158 return nil
159}
160
[181]161func (uc *upstreamConn) endPendingLISTs(all bool) (found bool) {
[177]162 found = false
163 for i := 0; i < len(uc.user.pendingLISTs); i++ {
164 pl := uc.user.pendingLISTs[i]
165 if _, ok := pl.pendingCommands[uc.network.ID]; !ok {
166 continue
167 }
168 delete(pl.pendingCommands, uc.network.ID)
169 if len(pl.pendingCommands) == 0 {
170 uc.user.pendingLISTs = append(uc.user.pendingLISTs[:i], uc.user.pendingLISTs[i+1:]...)
171 i--
172 uc.forEachDownstreamByID(pl.downstreamID, func(dc *downstreamConn) {
173 dc.SendMessage(&irc.Message{
174 Prefix: dc.srv.prefix(),
175 Command: irc.RPL_LISTEND,
176 Params: []string{dc.nick, "End of /LIST"},
177 })
178 })
179 }
180 found = true
181 if !all {
182 delete(uc.pendingLISTDownstreamSet, pl.downstreamID)
183 uc.user.forEachUpstream(func(uc *upstreamConn) {
[181]184 uc.trySendLIST(pl.downstreamID)
[177]185 })
186 return
187 }
188 }
189 return
190}
191
[181]192func (uc *upstreamConn) trySendLIST(downstreamID uint64) {
[177]193 if _, ok := uc.pendingLISTDownstreamSet[downstreamID]; ok {
194 // a LIST command is already pending
195 // we will try again when that command is completed
196 return
197 }
198
199 for _, pl := range uc.user.pendingLISTs {
200 if pl.downstreamID != downstreamID {
201 continue
202 }
203 // this is the first pending LIST command list of the downstream
204 listCommand, ok := pl.pendingCommands[uc.network.ID]
205 if !ok {
206 // there is no command for this upstream in these LIST commands
207 // do not send anything
208 continue
209 }
210 // there is a command for this upstream in these LIST commands
211 // send it now
212
213 uc.SendMessageLabeled(downstreamID, listCommand)
214
215 uc.pendingLISTDownstreamSet[downstreamID] = struct{}{}
216 return
217 }
218}
219
[139]220func (uc *upstreamConn) parseMembershipPrefix(s string) (membership *membership, nick string) {
221 for _, m := range uc.availableMemberships {
222 if m.Prefix == s[0] {
223 return &m, s[1:]
224 }
225 }
226 return nil, s
227}
228
[55]229func (uc *upstreamConn) handleMessage(msg *irc.Message) error {
[155]230 var label string
231 if l, ok := msg.GetTag("label"); ok {
232 label = l
233 }
234
[153]235 var msgBatch *batch
236 if batchName, ok := msg.GetTag("batch"); ok {
237 b, ok := uc.batches[batchName]
238 if !ok {
239 return fmt.Errorf("unexpected batch reference: batch was not defined: %q", batchName)
240 }
241 msgBatch = &b
[155]242 if label == "" {
243 label = msgBatch.Label
244 }
[153]245 }
246
[161]247 var downstreamID uint64 = 0
[155]248 if label != "" {
249 var labelOffset uint64
[161]250 n, err := fmt.Sscanf(label, "sd-%d-%d", &downstreamID, &labelOffset)
[155]251 if err == nil && n < 2 {
252 err = errors.New("not enough arguments")
253 }
254 if err != nil {
255 return fmt.Errorf("unexpected message label: invalid downstream reference for label %q: %v", label, err)
256 }
257 }
258
[216]259 if _, ok := msg.Tags["time"]; !ok {
[240]260 msg.Tags["time"] = irc.TagValue(time.Now().UTC().Format(serverTimeLayout))
[216]261 }
262
[13]263 switch msg.Command {
264 case "PING":
[60]265 uc.SendMessage(&irc.Message{
[13]266 Command: "PONG",
[68]267 Params: msg.Params,
[60]268 })
[33]269 return nil
[18]270 case "NOTICE":
[273]271 if msg.Prefix == nil {
272 return fmt.Errorf("expected a prefix")
273 }
274
[171]275 if msg.Prefix.User == "" && msg.Prefix.Host == "" { // server message
[239]276 uc.produce("", msg, nil)
[171]277 } else { // regular user NOTICE
[217]278 var entity, text string
279 if err := parseMessageParams(msg, &entity, &text); err != nil {
[171]280 return err
281 }
282
[217]283 target := entity
284 if target == uc.nick {
[178]285 target = msg.Prefix.Name
286 }
[239]287 uc.produce(target, msg, nil)
[171]288 }
[92]289 case "CAP":
[95]290 var subCmd string
291 if err := parseMessageParams(msg, nil, &subCmd); err != nil {
292 return err
[92]293 }
[95]294 subCmd = strings.ToUpper(subCmd)
295 subParams := msg.Params[2:]
296 switch subCmd {
297 case "LS":
298 if len(subParams) < 1 {
299 return newNeedMoreParamsError(msg.Command)
300 }
301 caps := strings.Fields(subParams[len(subParams)-1])
302 more := len(subParams) >= 2 && msg.Params[len(subParams)-2] == "*"
[92]303
[95]304 for _, s := range caps {
305 kv := strings.SplitN(s, "=", 2)
306 k := strings.ToLower(kv[0])
307 var v string
308 if len(kv) == 2 {
309 v = kv[1]
310 }
[277]311 uc.supportedCaps[k] = v
[92]312 }
313
[95]314 if more {
315 break // wait to receive all capabilities
316 }
317
[152]318 requestCaps := make([]string, 0, 16)
[276]319 for _, c := range []string{"message-tags", "batch", "labeled-response", "server-time", "away-notify"} {
[277]320 if _, ok := uc.supportedCaps[c]; ok {
[152]321 requestCaps = append(requestCaps, c)
322 }
323 }
324
[95]325 if uc.requestSASL() {
[152]326 requestCaps = append(requestCaps, "sasl")
327 }
328
329 if len(requestCaps) > 0 {
[95]330 uc.SendMessage(&irc.Message{
331 Command: "CAP",
[152]332 Params: []string{"REQ", strings.Join(requestCaps, " ")},
[95]333 })
[152]334 }
335
336 if uc.requestSASL() {
[95]337 break // we'll send CAP END after authentication is completed
338 }
339
[92]340 uc.SendMessage(&irc.Message{
341 Command: "CAP",
342 Params: []string{"END"},
343 })
[95]344 case "ACK", "NAK":
345 if len(subParams) < 1 {
346 return newNeedMoreParamsError(msg.Command)
347 }
348 caps := strings.Fields(subParams[0])
349
350 for _, name := range caps {
351 if err := uc.handleCapAck(strings.ToLower(name), subCmd == "ACK"); err != nil {
352 return err
353 }
354 }
355
356 if uc.saslClient == nil {
357 uc.SendMessage(&irc.Message{
358 Command: "CAP",
359 Params: []string{"END"},
360 })
361 }
362 default:
363 uc.logger.Printf("unhandled message: %v", msg)
[92]364 }
[95]365 case "AUTHENTICATE":
366 if uc.saslClient == nil {
367 return fmt.Errorf("received unexpected AUTHENTICATE message")
368 }
369
370 // TODO: if a challenge is 400 bytes long, buffer it
371 var challengeStr string
372 if err := parseMessageParams(msg, &challengeStr); err != nil {
373 uc.SendMessage(&irc.Message{
374 Command: "AUTHENTICATE",
375 Params: []string{"*"},
376 })
377 return err
378 }
379
380 var challenge []byte
381 if challengeStr != "+" {
382 var err error
383 challenge, err = base64.StdEncoding.DecodeString(challengeStr)
384 if err != nil {
385 uc.SendMessage(&irc.Message{
386 Command: "AUTHENTICATE",
387 Params: []string{"*"},
388 })
389 return err
390 }
391 }
392
393 var resp []byte
394 var err error
395 if !uc.saslStarted {
396 _, resp, err = uc.saslClient.Start()
397 uc.saslStarted = true
398 } else {
399 resp, err = uc.saslClient.Next(challenge)
400 }
401 if err != nil {
402 uc.SendMessage(&irc.Message{
403 Command: "AUTHENTICATE",
404 Params: []string{"*"},
405 })
406 return err
407 }
408
409 // TODO: send response in multiple chunks if >= 400 bytes
410 var respStr = "+"
411 if resp != nil {
412 respStr = base64.StdEncoding.EncodeToString(resp)
413 }
414
415 uc.SendMessage(&irc.Message{
416 Command: "AUTHENTICATE",
417 Params: []string{respStr},
418 })
[125]419 case irc.RPL_LOGGEDIN:
[95]420 var account string
421 if err := parseMessageParams(msg, nil, nil, &account); err != nil {
422 return err
423 }
424 uc.logger.Printf("logged in with account %q", account)
[125]425 case irc.RPL_LOGGEDOUT:
[95]426 uc.logger.Printf("logged out")
[125]427 case irc.ERR_NICKLOCKED, irc.RPL_SASLSUCCESS, irc.ERR_SASLFAIL, irc.ERR_SASLTOOLONG, irc.ERR_SASLABORTED:
[95]428 var info string
429 if err := parseMessageParams(msg, nil, &info); err != nil {
430 return err
431 }
432 switch msg.Command {
[125]433 case irc.ERR_NICKLOCKED:
[95]434 uc.logger.Printf("invalid nick used with SASL authentication: %v", info)
[125]435 case irc.ERR_SASLFAIL:
[95]436 uc.logger.Printf("SASL authentication failed: %v", info)
[125]437 case irc.ERR_SASLTOOLONG:
[95]438 uc.logger.Printf("SASL message too long: %v", info)
439 }
440
441 uc.saslClient = nil
442 uc.saslStarted = false
443
444 uc.SendMessage(&irc.Message{
445 Command: "CAP",
446 Params: []string{"END"},
447 })
[14]448 case irc.RPL_WELCOME:
[55]449 uc.registered = true
450 uc.logger.Printf("connection registered")
[19]451
[276]452 uc.forEachDownstream(func(dc *downstreamConn) {
453 dc.updateSupportedCaps()
454 })
455
[267]456 for _, ch := range uc.network.channels {
[146]457 params := []string{ch.Name}
458 if ch.Key != "" {
459 params = append(params, ch.Key)
460 }
[60]461 uc.SendMessage(&irc.Message{
[19]462 Command: "JOIN",
[146]463 Params: params,
[60]464 })
[19]465 }
[16]466 case irc.RPL_MYINFO:
[139]467 if err := parseMessageParams(msg, nil, &uc.serverName, nil, &uc.availableUserModes, nil); err != nil {
[43]468 return err
[16]469 }
[139]470 case irc.RPL_ISUPPORT:
471 if err := parseMessageParams(msg, nil, nil); err != nil {
472 return err
[16]473 }
[139]474 for _, token := range msg.Params[1 : len(msg.Params)-1] {
475 negate := false
476 parameter := token
477 value := ""
478 if strings.HasPrefix(token, "-") {
479 negate = true
480 token = token[1:]
481 } else {
482 if i := strings.IndexByte(token, '='); i >= 0 {
483 parameter = token[:i]
484 value = token[i+1:]
485 }
486 }
487 if !negate {
488 switch parameter {
489 case "CHANMODES":
490 parts := strings.SplitN(value, ",", 5)
491 if len(parts) < 4 {
492 return fmt.Errorf("malformed ISUPPORT CHANMODES value: %v", value)
493 }
494 modes := make(map[byte]channelModeType)
495 for i, mt := range []channelModeType{modeTypeA, modeTypeB, modeTypeC, modeTypeD} {
496 for j := 0; j < len(parts[i]); j++ {
497 mode := parts[i][j]
498 modes[mode] = mt
499 }
500 }
501 uc.availableChannelModes = modes
502 case "CHANTYPES":
503 uc.availableChannelTypes = value
504 case "PREFIX":
505 if value == "" {
506 uc.availableMemberships = nil
507 } else {
508 if value[0] != '(' {
509 return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
510 }
511 sep := strings.IndexByte(value, ')')
512 if sep < 0 || len(value) != sep*2 {
513 return fmt.Errorf("malformed ISUPPORT PREFIX value: %v", value)
514 }
515 memberships := make([]membership, len(value)/2-1)
516 for i := range memberships {
517 memberships[i] = membership{
518 Mode: value[i+1],
519 Prefix: value[sep+i+1],
520 }
521 }
522 uc.availableMemberships = memberships
523 }
524 }
525 } else {
526 // TODO: handle ISUPPORT negations
527 }
528 }
[153]529 case "BATCH":
530 var tag string
531 if err := parseMessageParams(msg, &tag); err != nil {
532 return err
533 }
534
535 if strings.HasPrefix(tag, "+") {
536 tag = tag[1:]
537 if _, ok := uc.batches[tag]; ok {
538 return fmt.Errorf("unexpected BATCH reference tag: batch was already defined: %q", tag)
539 }
540 var batchType string
541 if err := parseMessageParams(msg, nil, &batchType); err != nil {
542 return err
543 }
[155]544 label := label
545 if label == "" && msgBatch != nil {
546 label = msgBatch.Label
547 }
[153]548 uc.batches[tag] = batch{
549 Type: batchType,
550 Params: msg.Params[2:],
551 Outer: msgBatch,
[155]552 Label: label,
[153]553 }
554 } else if strings.HasPrefix(tag, "-") {
555 tag = tag[1:]
556 if _, ok := uc.batches[tag]; !ok {
557 return fmt.Errorf("unknown BATCH reference tag: %q", tag)
558 }
559 delete(uc.batches, tag)
560 } else {
561 return fmt.Errorf("unexpected BATCH reference tag: missing +/- prefix: %q", tag)
562 }
[42]563 case "NICK":
[83]564 if msg.Prefix == nil {
565 return fmt.Errorf("expected a prefix")
566 }
567
[43]568 var newNick string
569 if err := parseMessageParams(msg, &newNick); err != nil {
570 return err
[42]571 }
572
[244]573 me := false
[55]574 if msg.Prefix.Name == uc.nick {
575 uc.logger.Printf("changed nick from %q to %q", uc.nick, newNick)
[244]576 me = true
[55]577 uc.nick = newNick
[42]578 }
579
[55]580 for _, ch := range uc.channels {
[42]581 if membership, ok := ch.Members[msg.Prefix.Name]; ok {
582 delete(ch.Members, msg.Prefix.Name)
583 ch.Members[newNick] = membership
[215]584 uc.appendLog(ch.Name, msg)
[253]585 uc.appendHistory(ch.Name, msg)
[42]586 }
587 }
[82]588
[244]589 if !me {
[82]590 uc.forEachDownstream(func(dc *downstreamConn) {
[261]591 dc.SendMessage(dc.marshalMessage(msg, uc.network))
[82]592 })
593 }
[69]594 case "JOIN":
595 if msg.Prefix == nil {
596 return fmt.Errorf("expected a prefix")
597 }
[42]598
[43]599 var channels string
600 if err := parseMessageParams(msg, &channels); err != nil {
601 return err
[19]602 }
[34]603
[43]604 for _, ch := range strings.Split(channels, ",") {
[55]605 if msg.Prefix.Name == uc.nick {
606 uc.logger.Printf("joined channel %q", ch)
607 uc.channels[ch] = &upstreamChannel{
[34]608 Name: ch,
[55]609 conn: uc,
[139]610 Members: make(map[string]*membership),
[34]611 }
[139]612
613 uc.SendMessage(&irc.Message{
614 Command: "MODE",
615 Params: []string{ch},
616 })
[34]617 } else {
[55]618 ch, err := uc.getChannel(ch)
[34]619 if err != nil {
620 return err
621 }
[139]622 ch.Members[msg.Prefix.Name] = nil
[19]623 }
[69]624
[245]625 chMsg := msg.Copy()
626 chMsg.Params[0] = ch
627 uc.produce(ch, chMsg, nil)
[19]628 }
[69]629 case "PART":
630 if msg.Prefix == nil {
631 return fmt.Errorf("expected a prefix")
632 }
[34]633
[43]634 var channels string
635 if err := parseMessageParams(msg, &channels); err != nil {
636 return err
[34]637 }
638
[43]639 for _, ch := range strings.Split(channels, ",") {
[55]640 if msg.Prefix.Name == uc.nick {
641 uc.logger.Printf("parted channel %q", ch)
642 delete(uc.channels, ch)
[34]643 } else {
[55]644 ch, err := uc.getChannel(ch)
[34]645 if err != nil {
646 return err
647 }
648 delete(ch.Members, msg.Prefix.Name)
649 }
[69]650
[245]651 chMsg := msg.Copy()
652 chMsg.Params[0] = ch
653 uc.produce(ch, chMsg, nil)
[34]654 }
[159]655 case "KICK":
656 if msg.Prefix == nil {
657 return fmt.Errorf("expected a prefix")
658 }
659
660 var channel, user string
661 if err := parseMessageParams(msg, &channel, &user); err != nil {
662 return err
663 }
664
665 if user == uc.nick {
666 uc.logger.Printf("kicked from channel %q by %s", channel, msg.Prefix.Name)
667 delete(uc.channels, channel)
668 } else {
669 ch, err := uc.getChannel(channel)
670 if err != nil {
671 return err
672 }
673 delete(ch.Members, user)
674 }
675
[245]676 uc.produce(channel, msg, nil)
[83]677 case "QUIT":
678 if msg.Prefix == nil {
679 return fmt.Errorf("expected a prefix")
680 }
681
682 if msg.Prefix.Name == uc.nick {
683 uc.logger.Printf("quit")
684 }
685
686 for _, ch := range uc.channels {
[178]687 if _, ok := ch.Members[msg.Prefix.Name]; ok {
688 delete(ch.Members, msg.Prefix.Name)
689
[215]690 uc.appendLog(ch.Name, msg)
[253]691 uc.appendHistory(ch.Name, msg)
[178]692 }
[83]693 }
694
695 if msg.Prefix.Name != uc.nick {
696 uc.forEachDownstream(func(dc *downstreamConn) {
[261]697 dc.SendMessage(dc.marshalMessage(msg, uc.network))
[83]698 })
699 }
[19]700 case irc.RPL_TOPIC, irc.RPL_NOTOPIC:
[43]701 var name, topic string
702 if err := parseMessageParams(msg, nil, &name, &topic); err != nil {
703 return err
[19]704 }
[55]705 ch, err := uc.getChannel(name)
[19]706 if err != nil {
707 return err
708 }
709 if msg.Command == irc.RPL_TOPIC {
[43]710 ch.Topic = topic
[19]711 } else {
712 ch.Topic = ""
713 }
714 case "TOPIC":
[43]715 var name string
[74]716 if err := parseMessageParams(msg, &name); err != nil {
[43]717 return err
[19]718 }
[55]719 ch, err := uc.getChannel(name)
[19]720 if err != nil {
721 return err
722 }
723 if len(msg.Params) > 1 {
724 ch.Topic = msg.Params[1]
725 } else {
726 ch.Topic = ""
727 }
[245]728 uc.produce(ch.Name, msg, nil)
[139]729 case "MODE":
730 var name, modeStr string
731 if err := parseMessageParams(msg, &name, &modeStr); err != nil {
732 return err
733 }
734
735 if !uc.isChannel(name) { // user mode change
736 if name != uc.nick {
737 return fmt.Errorf("received MODE message for unknown nick %q", name)
738 }
739 return uc.modes.Apply(modeStr)
740 // TODO: notify downstreams about user mode change?
741 } else { // channel mode change
742 ch, err := uc.getChannel(name)
743 if err != nil {
744 return err
745 }
746
747 if ch.modes != nil {
748 if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[2:]...); err != nil {
749 return err
750 }
751 }
752
[245]753 uc.produce(ch.Name, msg, nil)
[139]754 }
755 case irc.RPL_UMODEIS:
756 if err := parseMessageParams(msg, nil); err != nil {
757 return err
758 }
759 modeStr := ""
760 if len(msg.Params) > 1 {
761 modeStr = msg.Params[1]
762 }
763
764 uc.modes = ""
765 if err := uc.modes.Apply(modeStr); err != nil {
766 return err
767 }
768 // TODO: send RPL_UMODEIS to downstream connections when applicable
769 case irc.RPL_CHANNELMODEIS:
770 var channel string
771 if err := parseMessageParams(msg, nil, &channel); err != nil {
772 return err
773 }
774 modeStr := ""
775 if len(msg.Params) > 2 {
776 modeStr = msg.Params[2]
777 }
778
779 ch, err := uc.getChannel(channel)
780 if err != nil {
781 return err
782 }
783
784 firstMode := ch.modes == nil
785 ch.modes = make(map[byte]string)
786 if err := ch.modes.Apply(uc.availableChannelModes, modeStr, msg.Params[3:]...); err != nil {
787 return err
788 }
789 if firstMode {
790 modeStr, modeParams := ch.modes.Format()
791
792 uc.forEachDownstream(func(dc *downstreamConn) {
[260]793 params := []string{dc.nick, dc.marshalEntity(uc.network, channel), modeStr}
[139]794 params = append(params, modeParams...)
795
796 dc.SendMessage(&irc.Message{
797 Prefix: dc.srv.prefix(),
798 Command: irc.RPL_CHANNELMODEIS,
799 Params: params,
800 })
801 })
802 }
[162]803 case rpl_creationtime:
804 var channel, creationTime string
805 if err := parseMessageParams(msg, nil, &channel, &creationTime); err != nil {
806 return err
807 }
808
809 ch, err := uc.getChannel(channel)
810 if err != nil {
811 return err
812 }
813
814 firstCreationTime := ch.creationTime == ""
815 ch.creationTime = creationTime
816 if firstCreationTime {
817 uc.forEachDownstream(func(dc *downstreamConn) {
818 dc.SendMessage(&irc.Message{
819 Prefix: dc.srv.prefix(),
820 Command: rpl_creationtime,
821 Params: []string{dc.nick, channel, creationTime},
822 })
823 })
824 }
[19]825 case rpl_topicwhotime:
[43]826 var name, who, timeStr string
827 if err := parseMessageParams(msg, nil, &name, &who, &timeStr); err != nil {
828 return err
[19]829 }
[55]830 ch, err := uc.getChannel(name)
[19]831 if err != nil {
832 return err
833 }
[43]834 ch.TopicWho = who
835 sec, err := strconv.ParseInt(timeStr, 10, 64)
[19]836 if err != nil {
837 return fmt.Errorf("failed to parse topic time: %v", err)
838 }
839 ch.TopicTime = time.Unix(sec, 0)
[177]840 case irc.RPL_LIST:
841 var channel, clients, topic string
842 if err := parseMessageParams(msg, nil, &channel, &clients, &topic); err != nil {
843 return err
844 }
845
[181]846 pl := uc.getPendingLIST()
[177]847 if pl == nil {
848 return fmt.Errorf("unexpected RPL_LIST: no matching pending LIST")
849 }
850
851 uc.forEachDownstreamByID(pl.downstreamID, func(dc *downstreamConn) {
852 dc.SendMessage(&irc.Message{
853 Prefix: dc.srv.prefix(),
854 Command: irc.RPL_LIST,
[260]855 Params: []string{dc.nick, dc.marshalEntity(uc.network, channel), clients, topic},
[177]856 })
857 })
858 case irc.RPL_LISTEND:
[181]859 ok := uc.endPendingLISTs(false)
[177]860 if !ok {
861 return fmt.Errorf("unexpected RPL_LISTEND: no matching pending LIST")
862 }
[19]863 case irc.RPL_NAMREPLY:
[43]864 var name, statusStr, members string
865 if err := parseMessageParams(msg, nil, &statusStr, &name, &members); err != nil {
866 return err
[19]867 }
[140]868
869 ch, ok := uc.channels[name]
870 if !ok {
871 // NAMES on a channel we have not joined, forward to downstream
[161]872 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]873 channel := dc.marshalEntity(uc.network, name)
[174]874 members := splitSpace(members)
[140]875 for i, member := range members {
876 membership, nick := uc.parseMembershipPrefix(member)
[260]877 members[i] = membership.String() + dc.marshalEntity(uc.network, nick)
[140]878 }
879 memberStr := strings.Join(members, " ")
880
881 dc.SendMessage(&irc.Message{
882 Prefix: dc.srv.prefix(),
883 Command: irc.RPL_NAMREPLY,
884 Params: []string{dc.nick, statusStr, channel, memberStr},
885 })
886 })
887 return nil
[19]888 }
889
[43]890 status, err := parseChannelStatus(statusStr)
[19]891 if err != nil {
892 return err
893 }
894 ch.Status = status
895
[174]896 for _, s := range splitSpace(members) {
[139]897 membership, nick := uc.parseMembershipPrefix(s)
[19]898 ch.Members[nick] = membership
899 }
900 case irc.RPL_ENDOFNAMES:
[43]901 var name string
902 if err := parseMessageParams(msg, nil, &name); err != nil {
903 return err
[25]904 }
[140]905
906 ch, ok := uc.channels[name]
907 if !ok {
908 // NAMES on a channel we have not joined, forward to downstream
[161]909 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]910 channel := dc.marshalEntity(uc.network, name)
[140]911
912 dc.SendMessage(&irc.Message{
913 Prefix: dc.srv.prefix(),
914 Command: irc.RPL_ENDOFNAMES,
915 Params: []string{dc.nick, channel, "End of /NAMES list"},
916 })
917 })
918 return nil
[25]919 }
920
[34]921 if ch.complete {
922 return fmt.Errorf("received unexpected RPL_ENDOFNAMES")
923 }
[25]924 ch.complete = true
[27]925
[73]926 uc.forEachDownstream(func(dc *downstreamConn) {
[27]927 forwardChannel(dc, ch)
[40]928 })
[127]929 case irc.RPL_WHOREPLY:
930 var channel, username, host, server, nick, mode, trailing string
931 if err := parseMessageParams(msg, nil, &channel, &username, &host, &server, &nick, &mode, &trailing); err != nil {
932 return err
933 }
934
935 parts := strings.SplitN(trailing, " ", 2)
936 if len(parts) != 2 {
937 return fmt.Errorf("received malformed RPL_WHOREPLY: wrong trailing parameter: %s", trailing)
938 }
939 realname := parts[1]
940 hops, err := strconv.Atoi(parts[0])
941 if err != nil {
942 return fmt.Errorf("received malformed RPL_WHOREPLY: wrong hop count: %s", parts[0])
943 }
944 hops++
945
946 trailing = strconv.Itoa(hops) + " " + realname
947
[161]948 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[127]949 channel := channel
950 if channel != "*" {
[260]951 channel = dc.marshalEntity(uc.network, channel)
[127]952 }
[260]953 nick := dc.marshalEntity(uc.network, nick)
[127]954 dc.SendMessage(&irc.Message{
955 Prefix: dc.srv.prefix(),
956 Command: irc.RPL_WHOREPLY,
957 Params: []string{dc.nick, channel, username, host, server, nick, mode, trailing},
958 })
959 })
960 case irc.RPL_ENDOFWHO:
961 var name string
962 if err := parseMessageParams(msg, nil, &name); err != nil {
963 return err
964 }
965
[161]966 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[127]967 name := name
968 if name != "*" {
969 // TODO: support WHO masks
[260]970 name = dc.marshalEntity(uc.network, name)
[127]971 }
972 dc.SendMessage(&irc.Message{
973 Prefix: dc.srv.prefix(),
974 Command: irc.RPL_ENDOFWHO,
[142]975 Params: []string{dc.nick, name, "End of /WHO list"},
[127]976 })
977 })
[128]978 case irc.RPL_WHOISUSER:
979 var nick, username, host, realname string
980 if err := parseMessageParams(msg, nil, &nick, &username, &host, nil, &realname); err != nil {
981 return err
982 }
983
[161]984 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]985 nick := dc.marshalEntity(uc.network, nick)
[128]986 dc.SendMessage(&irc.Message{
987 Prefix: dc.srv.prefix(),
988 Command: irc.RPL_WHOISUSER,
989 Params: []string{dc.nick, nick, username, host, "*", realname},
990 })
991 })
992 case irc.RPL_WHOISSERVER:
993 var nick, server, serverInfo string
994 if err := parseMessageParams(msg, nil, &nick, &server, &serverInfo); err != nil {
995 return err
996 }
997
[161]998 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]999 nick := dc.marshalEntity(uc.network, nick)
[128]1000 dc.SendMessage(&irc.Message{
1001 Prefix: dc.srv.prefix(),
1002 Command: irc.RPL_WHOISSERVER,
1003 Params: []string{dc.nick, nick, server, serverInfo},
1004 })
1005 })
1006 case irc.RPL_WHOISOPERATOR:
1007 var nick string
1008 if err := parseMessageParams(msg, nil, &nick); err != nil {
1009 return err
1010 }
1011
[161]1012 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]1013 nick := dc.marshalEntity(uc.network, nick)
[128]1014 dc.SendMessage(&irc.Message{
1015 Prefix: dc.srv.prefix(),
1016 Command: irc.RPL_WHOISOPERATOR,
1017 Params: []string{dc.nick, nick, "is an IRC operator"},
1018 })
1019 })
1020 case irc.RPL_WHOISIDLE:
1021 var nick string
1022 if err := parseMessageParams(msg, nil, &nick, nil); err != nil {
1023 return err
1024 }
1025
[161]1026 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]1027 nick := dc.marshalEntity(uc.network, nick)
[128]1028 params := []string{dc.nick, nick}
1029 params = append(params, msg.Params[2:]...)
1030 dc.SendMessage(&irc.Message{
1031 Prefix: dc.srv.prefix(),
1032 Command: irc.RPL_WHOISIDLE,
1033 Params: params,
1034 })
1035 })
1036 case irc.RPL_WHOISCHANNELS:
1037 var nick, channelList string
1038 if err := parseMessageParams(msg, nil, &nick, &channelList); err != nil {
1039 return err
1040 }
[174]1041 channels := splitSpace(channelList)
[128]1042
[161]1043 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]1044 nick := dc.marshalEntity(uc.network, nick)
[128]1045 channelList := make([]string, len(channels))
1046 for i, channel := range channels {
[139]1047 prefix, channel := uc.parseMembershipPrefix(channel)
[260]1048 channel = dc.marshalEntity(uc.network, channel)
[128]1049 channelList[i] = prefix.String() + channel
1050 }
1051 channels := strings.Join(channelList, " ")
1052 dc.SendMessage(&irc.Message{
1053 Prefix: dc.srv.prefix(),
1054 Command: irc.RPL_WHOISCHANNELS,
1055 Params: []string{dc.nick, nick, channels},
1056 })
1057 })
1058 case irc.RPL_ENDOFWHOIS:
1059 var nick string
1060 if err := parseMessageParams(msg, nil, &nick); err != nil {
1061 return err
1062 }
1063
[161]1064 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
[260]1065 nick := dc.marshalEntity(uc.network, nick)
[128]1066 dc.SendMessage(&irc.Message{
1067 Prefix: dc.srv.prefix(),
1068 Command: irc.RPL_ENDOFWHOIS,
[142]1069 Params: []string{dc.nick, nick, "End of /WHOIS list"},
[128]1070 })
1071 })
[36]1072 case "PRIVMSG":
[117]1073 if msg.Prefix == nil {
1074 return fmt.Errorf("expected a prefix")
1075 }
1076
[217]1077 var entity, text string
1078 if err := parseMessageParams(msg, &entity, &text); err != nil {
[69]1079 return err
1080 }
[117]1081
1082 if msg.Prefix.Name == serviceNick {
1083 uc.logger.Printf("skipping PRIVMSG from soju's service: %v", msg)
1084 break
1085 }
[217]1086 if entity == serviceNick {
[117]1087 uc.logger.Printf("skipping PRIVMSG to soju's service: %v", msg)
1088 break
1089 }
1090
[217]1091 target := entity
1092 if target == uc.nick {
[178]1093 target = msg.Prefix.Name
1094 }
[239]1095 uc.produce(target, msg, nil)
[115]1096 case "INVITE":
[273]1097 var nick, channel string
[115]1098 if err := parseMessageParams(msg, &nick, &channel); err != nil {
1099 return err
1100 }
1101
1102 uc.forEachDownstream(func(dc *downstreamConn) {
1103 dc.SendMessage(&irc.Message{
[260]1104 Prefix: dc.marshalUserPrefix(uc.network, msg.Prefix),
[115]1105 Command: "INVITE",
[260]1106 Params: []string{dc.marshalEntity(uc.network, nick), dc.marshalEntity(uc.network, channel)},
[115]1107 })
1108 })
[163]1109 case irc.RPL_INVITING:
[273]1110 var nick, channel string
[163]1111 if err := parseMessageParams(msg, &nick, &channel); err != nil {
1112 return err
1113 }
1114
1115 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1116 dc.SendMessage(&irc.Message{
1117 Prefix: dc.srv.prefix(),
1118 Command: irc.RPL_INVITING,
[260]1119 Params: []string{dc.nick, dc.marshalEntity(uc.network, nick), dc.marshalEntity(uc.network, channel)},
[163]1120 })
1121 })
[177]1122 case irc.ERR_UNKNOWNCOMMAND, irc.RPL_TRYAGAIN:
1123 var command, reason string
1124 if err := parseMessageParams(msg, nil, &command, &reason); err != nil {
1125 return err
1126 }
1127
1128 if command == "LIST" {
[181]1129 ok := uc.endPendingLISTs(false)
[177]1130 if !ok {
1131 return fmt.Errorf("unexpected response for LIST: %q: no matching pending LIST", msg.Command)
1132 }
1133 uc.forEachDownstreamByID(downstreamID, func(dc *downstreamConn) {
1134 dc.SendMessage(&irc.Message{
1135 Prefix: uc.srv.prefix(),
1136 Command: msg.Command,
1137 Params: []string{dc.nick, "LIST", reason},
1138 })
1139 })
1140 }
[272]1141 case irc.RPL_AWAY:
1142 var nick, reason string
1143 if err := parseMessageParams(msg, nil, &nick, &reason); err != nil {
1144 return err
1145 }
1146
[274]1147 uc.forEachDownstream(func(dc *downstreamConn) {
[272]1148 dc.SendMessage(&irc.Message{
1149 Prefix: dc.srv.prefix(),
1150 Command: irc.RPL_AWAY,
1151 Params: []string{dc.nick, dc.marshalEntity(uc.network, nick), reason},
1152 })
1153 })
[276]1154 case "AWAY":
1155 if msg.Prefix == nil {
1156 return fmt.Errorf("expected a prefix")
1157 }
1158
1159 uc.forEachDownstream(func(dc *downstreamConn) {
1160 if !dc.caps["away-notify"] {
1161 return
1162 }
1163 dc.SendMessage(&irc.Message{
1164 Prefix: dc.marshalUserPrefix(uc.network, msg.Prefix),
1165 Command: "AWAY",
1166 Params: msg.Params,
1167 })
1168 })
[152]1169 case "TAGMSG":
1170 // TODO: relay to downstream connections that accept message-tags
[155]1171 case "ACK":
1172 // Ignore
[198]1173 case irc.RPL_NOWAWAY, irc.RPL_UNAWAY:
1174 // Ignore
[16]1175 case irc.RPL_YOURHOST, irc.RPL_CREATED:
[14]1176 // Ignore
1177 case irc.RPL_LUSERCLIENT, irc.RPL_LUSEROP, irc.RPL_LUSERUNKNOWN, irc.RPL_LUSERCHANNELS, irc.RPL_LUSERME:
1178 // Ignore
1179 case irc.RPL_MOTDSTART, irc.RPL_MOTD, irc.RPL_ENDOFMOTD:
1180 // Ignore
[177]1181 case irc.RPL_LISTSTART:
1182 // Ignore
[14]1183 case rpl_localusers, rpl_globalusers:
1184 // Ignore
[96]1185 case irc.RPL_STATSVLINE, rpl_statsping, irc.RPL_STATSBLINE, irc.RPL_STATSDLINE:
[14]1186 // Ignore
[13]1187 default:
[95]1188 uc.logger.Printf("unhandled message: %v", msg)
[13]1189 }
[14]1190 return nil
[13]1191}
1192
[174]1193func splitSpace(s string) []string {
1194 return strings.FieldsFunc(s, func(r rune) bool {
1195 return r == ' '
1196 })
1197}
1198
[55]1199func (uc *upstreamConn) register() {
[77]1200 uc.nick = uc.network.Nick
1201 uc.username = uc.network.Username
1202 if uc.username == "" {
1203 uc.username = uc.nick
1204 }
1205 uc.realname = uc.network.Realname
1206 if uc.realname == "" {
1207 uc.realname = uc.nick
1208 }
1209
[60]1210 uc.SendMessage(&irc.Message{
[92]1211 Command: "CAP",
1212 Params: []string{"LS", "302"},
1213 })
1214
[93]1215 if uc.network.Pass != "" {
1216 uc.SendMessage(&irc.Message{
1217 Command: "PASS",
1218 Params: []string{uc.network.Pass},
1219 })
1220 }
1221
[92]1222 uc.SendMessage(&irc.Message{
[13]1223 Command: "NICK",
[69]1224 Params: []string{uc.nick},
[60]1225 })
1226 uc.SendMessage(&irc.Message{
[13]1227 Command: "USER",
[77]1228 Params: []string{uc.username, "0", "*", uc.realname},
[60]1229 })
[44]1230}
[13]1231
[197]1232func (uc *upstreamConn) runUntilRegistered() error {
1233 for !uc.registered {
[212]1234 msg, err := uc.ReadMessage()
[197]1235 if err != nil {
1236 return fmt.Errorf("failed to read message: %v", err)
1237 }
1238
1239 if err := uc.handleMessage(msg); err != nil {
1240 return fmt.Errorf("failed to handle message %q: %v", msg, err)
1241 }
1242 }
1243
[263]1244 for _, command := range uc.network.ConnectCommands {
1245 m, err := irc.ParseMessage(command)
1246 if err != nil {
1247 uc.logger.Printf("failed to parse connect command %q: %v", command, err)
1248 } else {
1249 uc.SendMessage(m)
1250 }
1251 }
1252
[197]1253 return nil
1254}
1255
[95]1256func (uc *upstreamConn) requestSASL() bool {
1257 if uc.network.SASL.Mechanism == "" {
1258 return false
1259 }
1260
[277]1261 v, ok := uc.supportedCaps["sasl"]
[95]1262 if !ok {
1263 return false
1264 }
1265 if v != "" {
1266 mechanisms := strings.Split(v, ",")
1267 found := false
1268 for _, mech := range mechanisms {
1269 if strings.EqualFold(mech, uc.network.SASL.Mechanism) {
1270 found = true
1271 break
1272 }
1273 }
1274 if !found {
1275 return false
1276 }
1277 }
1278
1279 return true
1280}
1281
1282func (uc *upstreamConn) handleCapAck(name string, ok bool) error {
[278]1283 uc.caps[name] = ok
1284
[95]1285 switch name {
1286 case "sasl":
1287 if !ok {
1288 uc.logger.Printf("server refused to acknowledge the SASL capability")
1289 return nil
1290 }
1291
[276]1292 auth := &uc.network.SASL
[95]1293 switch auth.Mechanism {
1294 case "PLAIN":
1295 uc.logger.Printf("starting SASL PLAIN authentication with username %q", auth.Plain.Username)
1296 uc.saslClient = sasl.NewPlainClient("", auth.Plain.Username, auth.Plain.Password)
1297 default:
1298 return fmt.Errorf("unsupported SASL mechanism %q", name)
1299 }
1300
1301 uc.SendMessage(&irc.Message{
1302 Command: "AUTHENTICATE",
1303 Params: []string{auth.Mechanism},
1304 })
[278]1305 case "message-tags", "labeled-response", "away-notify", "batch", "server-time":
[193]1306 // Nothing to do
1307 default:
1308 uc.logger.Printf("received CAP ACK/NAK for a cap we don't support: %v", name)
[95]1309 }
1310 return nil
1311}
1312
[165]1313func (uc *upstreamConn) readMessages(ch chan<- event) error {
[13]1314 for {
[210]1315 msg, err := uc.ReadMessage()
[13]1316 if err == io.EOF {
1317 break
1318 } else if err != nil {
1319 return fmt.Errorf("failed to read IRC command: %v", err)
1320 }
1321
[165]1322 ch <- eventUpstreamMessage{msg, uc}
[13]1323 }
1324
[45]1325 return nil
[13]1326}
[60]1327
[176]1328func (uc *upstreamConn) SendMessageLabeled(downstreamID uint64, msg *irc.Message) {
[278]1329 if uc.caps["labeled-response"] {
[155]1330 if msg.Tags == nil {
1331 msg.Tags = make(map[string]irc.TagValue)
1332 }
[176]1333 msg.Tags["label"] = irc.TagValue(fmt.Sprintf("sd-%d-%d", downstreamID, uc.nextLabelID))
[161]1334 uc.nextLabelID++
[155]1335 }
1336 uc.SendMessage(msg)
1337}
[178]1338
1339// TODO: handle moving logs when a network name changes, when support for this is added
[215]1340func (uc *upstreamConn) appendLog(entity string, msg *irc.Message) {
[178]1341 if uc.srv.LogPath == "" {
1342 return
1343 }
[215]1344
1345 ml, ok := uc.messageLoggers[entity]
1346 if !ok {
[248]1347 ml = newMessageLogger(uc.network, entity)
[215]1348 uc.messageLoggers[entity] = ml
[178]1349 }
1350
[215]1351 if err := ml.Append(msg); err != nil {
1352 uc.logger.Printf("failed to log message: %v", err)
[178]1353 }
1354}
[198]1355
[253]1356// appendHistory appends a message to the history. entity can be empty.
1357func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) {
1358 // If no client is offline, no need to append the message to the buffer
1359 if len(uc.network.offlineClients) == 0 {
1360 return
1361 }
1362
1363 history, ok := uc.network.history[entity]
1364 if !ok {
1365 history = &networkHistory{
1366 offlineClients: make(map[string]uint64),
1367 ring: NewRing(uc.srv.RingCap),
1368 }
1369 uc.network.history[entity] = history
1370
1371 for clientName, _ := range uc.network.offlineClients {
1372 history.offlineClients[clientName] = 0
1373 }
1374 }
1375
1376 history.ring.Produce(msg)
1377}
1378
[245]1379// produce appends a message to the logs, adds it to the history and forwards
1380// it to connected downstream connections.
1381//
1382// If origin is not nil and origin doesn't support echo-message, the message is
1383// forwarded to all connections except origin.
[239]1384func (uc *upstreamConn) produce(target string, msg *irc.Message, origin *downstreamConn) {
1385 if target != "" {
1386 uc.appendLog(target, msg)
1387 }
1388
[253]1389 uc.appendHistory(target, msg)
[227]1390
1391 uc.forEachDownstream(func(dc *downstreamConn) {
[238]1392 if dc != origin || dc.caps["echo-message"] {
[261]1393 dc.SendMessage(dc.marshalMessage(msg, uc.network))
[238]1394 }
[227]1395 })
[226]1396}
1397
[198]1398func (uc *upstreamConn) updateAway() {
1399 away := true
1400 uc.forEachDownstream(func(*downstreamConn) {
1401 away = false
1402 })
1403 if away == uc.away {
1404 return
1405 }
1406 if away {
1407 uc.SendMessage(&irc.Message{
1408 Command: "AWAY",
1409 Params: []string{"Auto away"},
1410 })
1411 } else {
1412 uc.SendMessage(&irc.Message{
1413 Command: "AWAY",
1414 })
1415 }
1416 uc.away = away
1417}
Note: See TracBrowser for help on using the repository browser.