1package sshchat 2 3import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/shazow/rateio" 13 "github.com/shazow/ssh-chat/chat" 14 "github.com/shazow/ssh-chat/chat/message" 15 "github.com/shazow/ssh-chat/internal/humantime" 16 "github.com/shazow/ssh-chat/internal/sanitize" 17 "github.com/shazow/ssh-chat/sshd" 18) 19 20const maxInputLength int = 1024 21 22// GetPrompt will render the terminal prompt string based on the user. 23func GetPrompt(user *message.User) string { 24 name := user.Name() 25 cfg := user.Config() 26 if cfg.Theme != nil { 27 name = cfg.Theme.ColorName(user) 28 } 29 return fmt.Sprintf("[%s] ", name) 30} 31 32// Host is the bridge between sshd and chat modules 33// TODO: Should be easy to add support for multiple rooms, if we want. 34type Host struct { 35 *chat.Room 36 listener *sshd.SSHListener 37 commands chat.Commands 38 auth *Auth 39 40 // Version string to print on /version 41 Version string 42 43 // Default theme 44 theme message.Theme 45 46 mu sync.Mutex 47 motd string 48 count int 49 50 // GetMOTD is used to reload the motd from an external source 51 GetMOTD func() (string, error) 52} 53 54// NewHost creates a Host on top of an existing listener. 55func NewHost(listener *sshd.SSHListener, auth *Auth) *Host { 56 room := chat.NewRoom() 57 h := Host{ 58 Room: room, 59 listener: listener, 60 commands: chat.Commands{}, 61 auth: auth, 62 } 63 64 // Make our own commands registry instance. 65 chat.InitCommands(&h.commands) 66 h.InitCommands(&h.commands) 67 room.SetCommands(h.commands) 68 69 go room.Serve() 70 return &h 71} 72 73// SetTheme sets the default theme for the host. 74func (h *Host) SetTheme(theme message.Theme) { 75 h.mu.Lock() 76 h.theme = theme 77 h.mu.Unlock() 78} 79 80// SetMotd sets the host's message of the day. 81// TODO: Change to SetMOTD 82func (h *Host) SetMotd(motd string) { 83 h.mu.Lock() 84 h.motd = motd 85 h.mu.Unlock() 86} 87 88func (h *Host) isOp(conn sshd.Connection) bool { 89 key := conn.PublicKey() 90 if key == nil { 91 return false 92 } 93 return h.auth.IsOp(key) 94} 95 96// Connect a specific Terminal to this host and its room. 97func (h *Host) Connect(term *sshd.Terminal) { 98 id := NewIdentity(term.Conn) 99 user := message.NewUserScreen(id, term) 100 user.OnChange = func() { 101 term.SetPrompt(GetPrompt(user)) 102 user.SetHighlight(user.ID()) 103 } 104 cfg := user.Config() 105 106 apiMode := strings.ToLower(term.Term()) == "bot" 107 108 if apiMode { 109 cfg.Theme = message.MonoTheme 110 cfg.Echo = false 111 } else { 112 term.SetEnterClear(true) // We provide our own echo rendering 113 cfg.Theme = &h.theme 114 } 115 116 user.SetConfig(cfg) 117 118 // Load user config overrides from ENV 119 // TODO: Would be nice to skip the command parsing pipeline just to load 120 // config values. Would need to factor out some command handler logic into 121 // accessible helpers. 122 env := term.Env() 123 for _, e := range env { 124 switch e.Key { 125 case "SSHCHAT_TIMESTAMP": 126 if e.Value != "" && e.Value != "0" { 127 cmd := "/timestamp" 128 if e.Value != "1" { 129 cmd += " " + e.Value 130 } 131 if msg, ok := message.NewPublicMsg(cmd, user).ParseCommand(); ok { 132 h.Room.HandleMsg(msg) 133 } 134 } 135 case "SSHCHAT_THEME": 136 cmd := "/theme " + e.Value 137 if msg, ok := message.NewPublicMsg(cmd, user).ParseCommand(); ok { 138 h.Room.HandleMsg(msg) 139 } 140 } 141 } 142 143 go user.Consume() 144 145 // Close term once user is closed. 146 defer user.Close() 147 defer term.Close() 148 149 h.mu.Lock() 150 motd := h.motd 151 count := h.count 152 h.count++ 153 h.mu.Unlock() 154 155 // Send MOTD 156 if motd != "" { 157 user.Send(message.NewAnnounceMsg(motd)) 158 } 159 160 member, err := h.Join(user) 161 if err != nil { 162 // Try again... 163 id.SetName(fmt.Sprintf("Guest%d", count)) 164 member, err = h.Join(user) 165 } 166 if err != nil { 167 logger.Errorf("[%s] Failed to join: %s", term.Conn.RemoteAddr(), err) 168 return 169 } 170 171 // Successfully joined. 172 if !apiMode { 173 term.SetPrompt(GetPrompt(user)) 174 term.AutoCompleteCallback = h.AutoCompleteFunction(user) 175 user.SetHighlight(user.Name()) 176 } 177 178 // Should the user be op'd on join? 179 if h.isOp(term.Conn) { 180 member.IsOp = true 181 } 182 ratelimit := rateio.NewSimpleLimiter(3, time.Second*3) 183 184 logger.Debugf("[%s] Joined: %s", term.Conn.RemoteAddr(), user.Name()) 185 186 for { 187 line, err := term.ReadLine() 188 if err == io.EOF { 189 // Closed 190 break 191 } else if err != nil { 192 logger.Errorf("[%s] Terminal reading error: %s", term.Conn.RemoteAddr(), err) 193 break 194 } 195 196 err = ratelimit.Count(1) 197 if err != nil { 198 user.Send(message.NewSystemMsg("Message rejected: Rate limiting is in effect.", user)) 199 continue 200 } 201 if len(line) > maxInputLength { 202 user.Send(message.NewSystemMsg("Message rejected: Input too long.", user)) 203 continue 204 } 205 if line == "" { 206 // Silently ignore empty lines. 207 term.Write([]byte{}) 208 continue 209 } 210 211 m := message.ParseInput(line, user) 212 213 if !apiMode { 214 if m, ok := m.(*message.CommandMsg); ok { 215 // Other messages render themselves by the room, commands we'll 216 // have to re-echo ourselves manually. 217 user.HandleMsg(m) 218 } 219 } 220 221 // FIXME: Any reason to use h.room.Send(m) instead? 222 h.HandleMsg(m) 223 224 if apiMode { 225 // Skip the remaining rendering workarounds 226 continue 227 } 228 } 229 230 err = h.Leave(user) 231 if err != nil { 232 logger.Errorf("[%s] Failed to leave: %s", term.Conn.RemoteAddr(), err) 233 return 234 } 235 logger.Debugf("[%s] Leaving: %s", term.Conn.RemoteAddr(), user.Name()) 236} 237 238// Serve our chat room onto the listener 239func (h *Host) Serve() { 240 h.listener.HandlerFunc = h.Connect 241 h.listener.Serve() 242} 243 244func (h *Host) completeName(partial string, skipName string) string { 245 names := h.NamesPrefix(partial) 246 if len(names) == 0 { 247 // Didn't find anything 248 return "" 249 } else if name := names[0]; name != skipName { 250 // First name is not the skipName, great 251 return name 252 } else if len(names) > 1 { 253 // Next candidate 254 return names[1] 255 } 256 return "" 257} 258 259func (h *Host) completeCommand(partial string) string { 260 for cmd := range h.commands { 261 if strings.HasPrefix(cmd, partial) { 262 return cmd 263 } 264 } 265 return "" 266} 267 268// AutoCompleteFunction returns a callback for terminal autocompletion 269func (h *Host) AutoCompleteFunction(u *message.User) func(line string, pos int, key rune) (newLine string, newPos int, ok bool) { 270 return func(line string, pos int, key rune) (newLine string, newPos int, ok bool) { 271 if key != 9 { 272 return 273 } 274 275 if line == "" || strings.HasSuffix(line[:pos], " ") { 276 // Don't autocomplete spaces. 277 return 278 } 279 280 fields := strings.Fields(line[:pos]) 281 isFirst := len(fields) < 2 282 partial := "" 283 if len(fields) > 0 { 284 partial = fields[len(fields)-1] 285 } 286 posPartial := pos - len(partial) 287 288 var completed string 289 if isFirst && strings.HasPrefix(line, "/") { 290 // Command 291 completed = h.completeCommand(partial) 292 if completed == "/reply" { 293 replyTo := u.ReplyTo() 294 if replyTo != nil { 295 name := replyTo.ID() 296 _, found := h.GetUser(name) 297 if found { 298 completed = "/msg " + name 299 } else { 300 u.SetReplyTo(nil) 301 } 302 } 303 } 304 } else { 305 // Name 306 completed = h.completeName(partial, u.Name()) 307 if completed == "" { 308 return 309 } 310 if isFirst { 311 completed += ":" 312 } 313 } 314 completed += " " 315 316 // Reposition the cursor 317 newLine = strings.Replace(line[posPartial:], partial, completed, 1) 318 newLine = line[:posPartial] + newLine 319 newPos = pos + (len(completed) - len(partial)) 320 ok = true 321 return 322 } 323} 324 325// GetUser returns a message.User based on a name. 326func (h *Host) GetUser(name string) (*message.User, bool) { 327 m, ok := h.MemberByID(name) 328 if !ok { 329 return nil, false 330 } 331 return m.User, true 332} 333 334// InitCommands adds host-specific commands to a Commands container. These will 335// override any existing commands. 336func (h *Host) InitCommands(c *chat.Commands) { 337 c.Add(chat.Command{ 338 Prefix: "/msg", 339 PrefixHelp: "USER MESSAGE", 340 Help: "Send MESSAGE to USER.", 341 Handler: func(room *chat.Room, msg message.CommandMsg) error { 342 args := msg.Args() 343 switch len(args) { 344 case 0: 345 return errors.New("must specify user") 346 case 1: 347 return errors.New("must specify message") 348 } 349 350 target, ok := h.GetUser(args[0]) 351 if !ok { 352 return errors.New("user not found") 353 } 354 355 m := message.NewPrivateMsg(strings.Join(args[1:], " "), msg.From(), target) 356 room.Send(&m) 357 358 txt := fmt.Sprintf("[Sent PM to %s]", target.Name()) 359 ms := message.NewSystemMsg(txt, msg.From()) 360 room.Send(ms) 361 target.SetReplyTo(msg.From()) 362 return nil 363 }, 364 }) 365 366 c.Add(chat.Command{ 367 Prefix: "/reply", 368 PrefixHelp: "MESSAGE", 369 Help: "Reply with MESSAGE to the previous private message.", 370 Handler: func(room *chat.Room, msg message.CommandMsg) error { 371 args := msg.Args() 372 switch len(args) { 373 case 0: 374 return errors.New("must specify message") 375 } 376 377 target := msg.From().ReplyTo() 378 if target == nil { 379 return errors.New("no message to reply to") 380 } 381 382 name := target.Name() 383 _, found := h.GetUser(name) 384 if !found { 385 return errors.New("user not found") 386 } 387 388 m := message.NewPrivateMsg(strings.Join(args, " "), msg.From(), target) 389 room.Send(&m) 390 391 txt := fmt.Sprintf("[Sent PM to %s]", name) 392 ms := message.NewSystemMsg(txt, msg.From()) 393 room.Send(ms) 394 target.SetReplyTo(msg.From()) 395 return nil 396 }, 397 }) 398 399 c.Add(chat.Command{ 400 Prefix: "/whois", 401 PrefixHelp: "USER", 402 Help: "Information about USER.", 403 Handler: func(room *chat.Room, msg message.CommandMsg) error { 404 args := msg.Args() 405 if len(args) == 0 { 406 return errors.New("must specify user") 407 } 408 409 target, ok := h.GetUser(args[0]) 410 if !ok { 411 return errors.New("user not found") 412 } 413 id := target.Identifier.(*Identity) 414 var whois string 415 switch room.IsOp(msg.From()) { 416 case true: 417 whois = id.WhoisAdmin(room) 418 case false: 419 whois = id.Whois() 420 } 421 room.Send(message.NewSystemMsg(whois, msg.From())) 422 423 return nil 424 }, 425 }) 426 427 // Hidden commands 428 c.Add(chat.Command{ 429 Prefix: "/version", 430 Handler: func(room *chat.Room, msg message.CommandMsg) error { 431 room.Send(message.NewSystemMsg(h.Version, msg.From())) 432 return nil 433 }, 434 }) 435 436 timeStarted := time.Now() 437 c.Add(chat.Command{ 438 Prefix: "/uptime", 439 Handler: func(room *chat.Room, msg message.CommandMsg) error { 440 room.Send(message.NewSystemMsg(humantime.Since(timeStarted), msg.From())) 441 return nil 442 }, 443 }) 444 445 // Op commands 446 c.Add(chat.Command{ 447 Op: true, 448 Prefix: "/kick", 449 PrefixHelp: "USER", 450 Help: "Kick USER from the server.", 451 Handler: func(room *chat.Room, msg message.CommandMsg) error { 452 if !room.IsOp(msg.From()) { 453 return errors.New("must be op") 454 } 455 456 args := msg.Args() 457 if len(args) == 0 { 458 return errors.New("must specify user") 459 } 460 461 target, ok := h.GetUser(args[0]) 462 if !ok { 463 return errors.New("user not found") 464 } 465 466 body := fmt.Sprintf("%s was kicked by %s.", target.Name(), msg.From().Name()) 467 room.Send(message.NewAnnounceMsg(body)) 468 target.Close() 469 return nil 470 }, 471 }) 472 473 c.Add(chat.Command{ 474 Op: true, 475 Prefix: "/ban", 476 PrefixHelp: "QUERY [DURATION]", 477 Help: "Ban from the server. QUERY can be a username to ban the fingerprint and ip, or quoted \"key=value\" pairs with keys like ip, fingerprint, client.", 478 Handler: func(room *chat.Room, msg message.CommandMsg) error { 479 // TODO: Would be nice to specify what to ban. Key? Ip? etc. 480 if !room.IsOp(msg.From()) { 481 return errors.New("must be op") 482 } 483 484 args := msg.Args() 485 if len(args) == 0 { 486 return errors.New("must specify user") 487 } 488 489 query := args[0] 490 target, ok := h.GetUser(query) 491 if !ok { 492 query = strings.Join(args, " ") 493 if strings.Contains(query, "=") { 494 return h.auth.BanQuery(query) 495 } 496 return errors.New("user not found") 497 } 498 499 var until time.Duration 500 if len(args) > 1 { 501 until, _ = time.ParseDuration(args[1]) 502 } 503 504 id := target.Identifier.(*Identity) 505 h.auth.Ban(id.PublicKey(), until) 506 h.auth.BanAddr(id.RemoteAddr(), until) 507 508 body := fmt.Sprintf("%s was banned by %s.", target.Name(), msg.From().Name()) 509 room.Send(message.NewAnnounceMsg(body)) 510 target.Close() 511 512 logger.Debugf("Banned: \n-> %s", id.Whois()) 513 514 return nil 515 }, 516 }) 517 518 c.Add(chat.Command{ 519 Op: true, 520 Prefix: "/banned", 521 Help: "List the current ban conditions.", 522 Handler: func(room *chat.Room, msg message.CommandMsg) error { 523 if !room.IsOp(msg.From()) { 524 return errors.New("must be op") 525 } 526 527 bannedIPs, bannedFingerprints, bannedClients := h.auth.Banned() 528 529 buf := bytes.Buffer{} 530 fmt.Fprintf(&buf, "Banned:") 531 for _, key := range bannedIPs { 532 fmt.Fprintf(&buf, "\n \"ip=%s\"", key) 533 } 534 for _, key := range bannedFingerprints { 535 fmt.Fprintf(&buf, "\n \"fingerprint=%s\"", key) 536 } 537 for _, key := range bannedClients { 538 fmt.Fprintf(&buf, "\n \"client=%s\"", key) 539 } 540 541 room.Send(message.NewSystemMsg(buf.String(), msg.From())) 542 543 return nil 544 }, 545 }) 546 547 c.Add(chat.Command{ 548 Op: true, 549 Prefix: "/motd", 550 PrefixHelp: "[MESSAGE]", 551 Help: "Set a new MESSAGE of the day, or print the motd if no MESSAGE.", 552 Handler: func(room *chat.Room, msg message.CommandMsg) error { 553 args := msg.Args() 554 user := msg.From() 555 556 h.mu.Lock() 557 motd := h.motd 558 h.mu.Unlock() 559 560 if len(args) == 0 { 561 room.Send(message.NewSystemMsg(motd, user)) 562 return nil 563 } 564 if !room.IsOp(user) { 565 return errors.New("must be OP to modify the MOTD") 566 } 567 568 var err error 569 var s string = strings.Join(args, " ") 570 571 if s == "@" { 572 if h.GetMOTD == nil { 573 return errors.New("motd reload not set") 574 } 575 if s, err = h.GetMOTD(); err != nil { 576 return err 577 } 578 } 579 580 h.SetMotd(s) 581 fromMsg := fmt.Sprintf("New message of the day set by %s:", msg.From().Name()) 582 room.Send(message.NewAnnounceMsg(fromMsg + message.Newline + "-> " + s)) 583 584 return nil 585 }, 586 }) 587 588 c.Add(chat.Command{ 589 Op: true, 590 Prefix: "/op", 591 PrefixHelp: "USER [DURATION|remove]", 592 Help: "Set USER as admin. Duration only applies to pubkey reconnects.", 593 Handler: func(room *chat.Room, msg message.CommandMsg) error { 594 if !room.IsOp(msg.From()) { 595 return errors.New("must be op") 596 } 597 598 args := msg.Args() 599 if len(args) == 0 { 600 return errors.New("must specify user") 601 } 602 603 opValue := true 604 var until time.Duration 605 if len(args) > 1 { 606 if args[1] == "remove" { 607 // Expire instantly 608 until = time.Duration(1) 609 opValue = false 610 } else { 611 until, _ = time.ParseDuration(args[1]) 612 } 613 } 614 615 member, ok := room.MemberByID(args[0]) 616 if !ok { 617 return errors.New("user not found") 618 } 619 member.IsOp = opValue 620 621 id := member.Identifier.(*Identity) 622 h.auth.Op(id.PublicKey(), until) 623 624 var body string 625 if opValue { 626 body = fmt.Sprintf("Made op by %s.", msg.From().Name()) 627 } else { 628 body = fmt.Sprintf("Removed op by %s.", msg.From().Name()) 629 } 630 room.Send(message.NewSystemMsg(body, member.User)) 631 632 return nil 633 }, 634 }) 635 636 c.Add(chat.Command{ 637 Op: true, 638 Prefix: "/rename", 639 PrefixHelp: "USER NEW_NAME [SYMBOL]", 640 Help: "Rename USER to NEW_NAME, add optional SYMBOL prefix", 641 Handler: func(room *chat.Room, msg message.CommandMsg) error { 642 if !room.IsOp(msg.From()) { 643 return errors.New("must be op") 644 } 645 646 args := msg.Args() 647 if len(args) < 2 { 648 return errors.New("must specify user and new name") 649 } 650 651 member, ok := room.MemberByID(args[0]) 652 if !ok { 653 return errors.New("user not found") 654 } 655 656 symbolSet := false 657 if len(args) == 3 { 658 s := args[2] 659 if id, ok := member.Identifier.(*Identity); ok { 660 id.SetSymbol(s) 661 } else { 662 return errors.New("user does not support setting symbol") 663 } 664 665 body := fmt.Sprintf("Assigned symbol %q by %s.", s, msg.From().Name()) 666 room.Send(message.NewSystemMsg(body, member.User)) 667 symbolSet = true 668 } 669 670 oldID := member.ID() 671 newID := sanitize.Name(args[1]) 672 if newID == oldID && !symbolSet { 673 return errors.New("new name is the same as the original") 674 } else if (newID == "" || newID == oldID) && symbolSet { 675 if member.User.OnChange != nil { 676 member.User.OnChange() 677 } 678 return nil 679 } 680 681 member.SetID(newID) 682 err := room.Rename(oldID, member) 683 if err != nil { 684 member.SetID(oldID) 685 return err 686 } 687 688 body := fmt.Sprintf("%s was renamed by %s.", oldID, msg.From().Name()) 689 room.Send(message.NewAnnounceMsg(body)) 690 691 return nil 692 }, 693 }) 694} 695