From ef2d54ebc86eff5ddebfbb96f4bdfbd1970b57e9 Mon Sep 17 00:00:00 2001 From: Peter Hellberg Date: Mon, 15 Dec 2014 01:12:19 +0100 Subject: [PATCH 1/3] Fix all lint issues (mainly ALL_CAPS and comments) --- client.go | 62 +++++++++++++++++++++++++++-------------- cmd.go | 3 +- colors.go | 46 ++++++++++++++++++++++--------- history.go | 5 ++++ server.go | 81 ++++++++++++++++++++++++++++++++++++------------------ 5 files changed, 137 insertions(+), 60 deletions(-) diff --git a/client.go b/client.go index 7e570c2..57d9406 100644 --- a/client.go +++ b/client.go @@ -9,10 +9,15 @@ import ( "golang.org/x/crypto/ssh/terminal" ) -const MSG_BUFFER int = 50 -const MAX_MSG_LENGTH int = 512 +const ( + // MsgBuffer is the length of the message buffer + MsgBuffer int = 50 -const HELP_TEXT string = SYSTEM_MESSAGE_FORMAT + `-> Available commands: + // MaxMsgLength is the maximum length of a message + MaxMsgLength int = 512 + + // HelpText is the text returned by /help + HelpText string = systemMessageFormat + `-> Available commands: /about - About this chat. /exit - Exit the chat. /help - Show this help text. @@ -23,18 +28,20 @@ const HELP_TEXT string = SYSTEM_MESSAGE_FORMAT + `-> Available commands: /whois $NAME - Display information about another connected user. /msg $NAME $MESSAGE - Sends a private message to a user. /motd - Prints the Message of the Day -` + RESET +` + Reset -const OP_HELP_TEXT string = SYSTEM_MESSAGE_FORMAT + `-> Available operator commands: + // OpHelpText is the additional text returned by /help if the client is an Op + OpHelpText string = systemMessageFormat + `-> Available operator commands: /ban $NAME - Banish a user from the chat /kick $NAME - Kick em' out. /op $NAME - Promote a user to server operator. /silence $NAME - Revoke a user's ability to speak. /motd $MESSAGE - Sets the Message of the Day /whitelist $FINGERPRINT - Adds pubkey fingerprint to the connection whitelist -` + RESET +` + Reset -const ABOUT_TEXT string = SYSTEM_MESSAGE_FORMAT + `-> ssh-chat is made by @shazow. + // AboutText is the text returned by /about + AboutText string = systemMessageFormat + `-> ssh-chat is made by @shazow. It is a custom ssh server built in Go to serve a chat experience instead of a shell. @@ -42,10 +49,13 @@ const ABOUT_TEXT string = SYSTEM_MESSAGE_FORMAT + `-> ssh-chat is made by @shazo Source: https://github.com/shazow/ssh-chat For more, visit shazow.net or follow at twitter.com/shazow -` + RESET +` + Reset -const REQUIRED_WAIT time.Duration = time.Second / 2 + // RequiredWait is the time a client is required to wait between messages + RequiredWait time.Duration = time.Second / 2 +) +// Client holds all the fields used by the client type Client struct { Server *Server Conn *ssh.ServerConn @@ -62,38 +72,44 @@ type Client struct { beepMe bool } +// NewClient constructs a new client func NewClient(server *Server, conn *ssh.ServerConn) *Client { return &Client{ Server: server, Conn: conn, Name: conn.User(), Color: RandomColor256(), - Msg: make(chan string, MSG_BUFFER), + Msg: make(chan string, MsgBuffer), ready: make(chan struct{}, 1), lastTX: time.Now(), } } +// ColoredName returns the client name in its color func (c *Client) ColoredName() string { return ColorString(c.Color, c.Name) } +// SysMsg sends a message in continuous format over the message channel func (c *Client) SysMsg(msg string, args ...interface{}) { - c.Msg <- ContinuousFormat(SYSTEM_MESSAGE_FORMAT, "-> "+fmt.Sprintf(msg, args...)) + c.Msg <- ContinuousFormat(systemMessageFormat, "-> "+fmt.Sprintf(msg, args...)) } +// Write writes the given message func (c *Client) Write(msg string) { c.term.Write([]byte(msg + "\r\n")) } +// WriteLines writes multiple messages func (c *Client) WriteLines(msg []string) { for _, line := range msg { c.Write(line) } } +// Send sends the given message func (c *Client) Send(msg string) { - if len(msg) > MAX_MSG_LENGTH { + if len(msg) > MaxMsgLength { return } select { @@ -104,21 +120,25 @@ func (c *Client) Send(msg string) { } } +// SendLines sends multiple messages func (c *Client) SendLines(msg []string) { for _, line := range msg { c.Send(line) } } +// IsSilenced checks if the client is silenced func (c *Client) IsSilenced() bool { return c.silencedUntil.After(time.Now()) } +// Silence silences a client for the given duration func (c *Client) Silence(d time.Duration) { c.silencedUntil = time.Now().Add(d) } -func (c *Client) Resize(width int, height int) error { +// Resize resizes the client to the given width and height +func (c *Client) Resize(width, height int) error { err := c.term.SetSize(width, height) if err != nil { logger.Errorf("Resize failed: %dx%d", width, height) @@ -128,11 +148,13 @@ func (c *Client) Resize(width int, height int) error { return nil } +// Rename renames the client to the given name func (c *Client) Rename(name string) { c.Name = name c.term.SetPrompt(fmt.Sprintf("[%s] ", c.ColoredName())) } +// Fingerprint returns the fingerprint func (c *Client) Fingerprint() string { return c.Conn.Permissions.Extensions["fingerprint"] } @@ -172,12 +194,12 @@ func (c *Client) handleShell(channel ssh.Channel) { case "/exit": channel.Close() case "/help": - c.WriteLines(strings.Split(HELP_TEXT, "\n")) + c.WriteLines(strings.Split(HelpText, "\n")) if c.Server.IsOp(c) { - c.WriteLines(strings.Split(OP_HELP_TEXT, "\n")) + c.WriteLines(strings.Split(OpHelpText, "\n")) } case "/about": - c.WriteLines(strings.Split(ABOUT_TEXT, "\n")) + c.WriteLines(strings.Split(AboutText, "\n")) case "/uptime": c.Write(c.Server.Uptime()) case "/beep": @@ -208,7 +230,7 @@ func (c *Client) handleShell(channel ssh.Channel) { if len(parts) == 2 { client := c.Server.Who(parts[1]) if client != nil { - version := RE_STRIP_TEXT.ReplaceAllString(string(client.Conn.ClientVersion()), "") + version := reStripText.ReplaceAllString(string(client.Conn.ClientVersion()), "") if len(version) > 100 { version = "Evil Jerk with a superlong string" } @@ -223,7 +245,7 @@ func (c *Client) handleShell(channel ssh.Channel) { names := "" nameList := c.Server.List(nil) for _, name := range nameList { - names += c.Server.Who(name).ColoredName() + SYSTEM_MESSAGE_FORMAT + ", " + names += c.Server.Who(name).ColoredName() + systemMessageFormat + ", " } if len(names) > 2 { names = names[:len(names)-2] @@ -317,7 +339,7 @@ func (c *Client) handleShell(channel ssh.Channel) { c.Server.MotdUnicast(c) } else { var newmotd string - if (len(parts) == 2) { + if len(parts) == 2 { newmotd = parts[1] } else { newmotd = parts[1] + " " + parts[2] @@ -344,7 +366,7 @@ func (c *Client) handleShell(channel ssh.Channel) { msg := fmt.Sprintf("%s: %s", c.ColoredName(), line) /* Rate limit */ - if time.Now().Sub(c.lastTX) < REQUIRED_WAIT { + if time.Now().Sub(c.lastTX) < RequiredWait { c.SysMsg("Rate limiting in effect.") continue } diff --git a/cmd.go b/cmd.go index cfedf85..9f5ea84 100644 --- a/cmd.go +++ b/cmd.go @@ -4,15 +4,16 @@ import ( "bufio" "fmt" "io/ioutil" - "strings" "os" "os/signal" + "strings" "github.com/alexcesaro/log" "github.com/alexcesaro/log/golog" "github.com/jessevdk/go-flags" ) +// Options contains the flag options type Options struct { Verbose []bool `short:"v" long:"verbose" description:"Show verbose logging."` Identity string `short:"i" long:"identity" description:"Private key to identify server with." default:"~/.ssh/id_rsa"` diff --git a/colors.go b/colors.go index 1e85441..64fc64e 100644 --- a/colors.go +++ b/colors.go @@ -2,40 +2,60 @@ package main import ( "fmt" + "math/rand" "strings" - "math/rand" "time" ) -const RESET string = "\033[0m" -const BOLD string = "\033[1m" -const DIM string = "\033[2m" -const ITALIC string = "\033[3m" -const UNDERLINE string = "\033[4m" -const BLINK string = "\033[5m" -const INVERT string = "\033[7m" +const ( + // Reset resets the color + Reset = "\033[0m" -var colors = []string { "31", "32", "33", "34", "35", "36", "37", "91", "92", "93", "94", "95", "96", "97" } + // Bold makes the following text bold + Bold = "\033[1m" + // Dim dims the following text + Dim = "\033[2m" + + // Italic makes the following text italic + Italic = "\033[3m" + + // Underline underlines the following text + Underline = "\033[4m" + + // Blink blinks the following text + Blink = "\033[5m" + + // Invert inverts the following text + Invert = "\033[7m" +) + +var colors = []string{"31", "32", "33", "34", "35", "36", "37", "91", "92", "93", "94", "95", "96", "97"} + +// RandomColor256 returns a random (of 256) color func RandomColor256() string { return fmt.Sprintf("38;05;%d", rand.Intn(256)) } +// RandomColor returns a random color func RandomColor() string { return colors[rand.Intn(len(colors))] } +// ColorString returns a message in the given color func ColorString(color string, msg string) string { - return BOLD + "\033[" + color + "m" + msg + RESET + return Bold + "\033[" + color + "m" + msg + Reset } +// RandomColorInit initializes the random seed func RandomColorInit() { rand.Seed(time.Now().UTC().UnixNano()) } -// Horrible hack to "continue" the previous string color and format -// after a RESET has been encountered. +// ContinuousFormat is a horrible hack to "continue" the previous string color +// and format after a RESET has been encountered. +// // This is not HTML where you can just do a to resume your previous formatting! func ContinuousFormat(format string, str string) string { - return SYSTEM_MESSAGE_FORMAT + strings.Replace(str, RESET, format, -1) + RESET + return systemMessageFormat + strings.Replace(str, Reset, format, -1) + Reset } diff --git a/history.go b/history.go index 698b72f..74ef513 100644 --- a/history.go +++ b/history.go @@ -3,6 +3,7 @@ package main import "sync" +// History contains the history entries type History struct { entries []string head int @@ -10,12 +11,14 @@ type History struct { lock sync.Mutex } +// NewHistory constructs a new history of the given size func NewHistory(size int) *History { return &History{ entries: make([]string, size), } } +// Add adds the given entry to the entries in the history func (h *History) Add(entry string) { h.lock.Lock() defer h.lock.Unlock() @@ -28,10 +31,12 @@ func (h *History) Add(entry string) { } } +// Len returns the number of entries in the history func (h *History) Len() int { return h.size } +// Get the entry with the given number func (h *History) Get(num int) []string { h.lock.Lock() defer h.lock.Unlock() diff --git a/server.go b/server.go index f360d13..dd94743 100644 --- a/server.go +++ b/server.go @@ -13,17 +13,22 @@ import ( "golang.org/x/crypto/ssh" ) -const MAX_NAME_LENGTH = 32 -const HISTORY_LEN = 20 +const ( + maxNameLength = 32 + historyLength = 20 + systemMessageFormat = "\033[1;3;90m" + privateMessageFormat = "\033[3m" + beep = "\007" +) -const SYSTEM_MESSAGE_FORMAT string = "\033[1;3;90m" -const PRIVATE_MESSAGE_FORMAT string = "\033[3m" -const BEEP string = "\007" - -var RE_STRIP_TEXT = regexp.MustCompile("[^0-9A-Za-z_.-]") +var ( + reStripText = regexp.MustCompile("[^0-9A-Za-z_.-]") +) +// Clients is a map of clients type Clients map[string]*Client +// Server holds all the fields used by a server type Server struct { sshConfig *ssh.ServerConfig done chan struct{} @@ -34,11 +39,12 @@ type Server struct { whitelist map[string]struct{} // fingerprint lookup admins map[string]struct{} // fingerprint lookup bannedPk map[string]*time.Time // fingerprint lookup - bannedIp map[net.Addr]*time.Time + bannedIP map[net.Addr]*time.Time started time.Time sync.Mutex } +// NewServer constructs a new server func NewServer(privateKey []byte) (*Server, error) { signer, err := ssh.ParsePrivateKey(privateKey) if err != nil { @@ -49,12 +55,12 @@ func NewServer(privateKey []byte) (*Server, error) { done: make(chan struct{}), clients: Clients{}, count: 0, - history: NewHistory(HISTORY_LEN), + history: NewHistory(historyLength), motd: "Message of the Day! Modify with /motd", whitelist: map[string]struct{}{}, admins: map[string]struct{}{}, bannedPk: map[string]*time.Time{}, - bannedIp: map[net.Addr]*time.Time{}, + bannedIP: map[net.Addr]*time.Time{}, started: time.Now(), } @@ -80,14 +86,17 @@ func NewServer(privateKey []byte) (*Server, error) { return &server, nil } +// Len returns the number of clients func (s *Server) Len() int { return len(s.clients) } +// SysMsg broadcasts the given message to everyone func (s *Server) SysMsg(msg string, args ...interface{}) { - s.Broadcast(ContinuousFormat(SYSTEM_MESSAGE_FORMAT, " * "+fmt.Sprintf(msg, args...)), nil) + s.Broadcast(ContinuousFormat(systemMessageFormat, " * "+fmt.Sprintf(msg, args...)), nil) } +// Broadcast broadcasts the given message to everyone except for the given client func (s *Server) Broadcast(msg string, except *Client) { logger.Debugf("Broadcast to %d: %s", s.Len(), msg) s.history.Add(msg) @@ -99,45 +108,49 @@ func (s *Server) Broadcast(msg string, except *Client) { if strings.Contains(msg, client.Name) { // Turn message red if client's name is mentioned, and send BEL if they have enabled beeping - tmpMsg := strings.Split(msg, RESET) + tmpMsg := strings.Split(msg, Reset) if client.beepMe { - tmpMsg[0] += BEEP + tmpMsg[0] += beep } - client.Send(strings.Join(tmpMsg, RESET + BOLD + "\033[31m") + RESET) + client.Send(strings.Join(tmpMsg, Reset+Bold+"\033[31m") + Reset) } else { client.Send(msg) } } } -/* Send a message to a particular nick, if it exists */ +// Privmsg sends a message to a particular nick, if it exists func (s *Server) Privmsg(nick, message string, sender *Client) error { - /* Get the recipient */ + // Get the recipient target, ok := s.clients[nick] if !ok { return fmt.Errorf("no client with that nick") } - /* Send the message */ - target.Msg <- fmt.Sprintf(BEEP+"[PM from %v] %s%v%s", sender.ColoredName(), PRIVATE_MESSAGE_FORMAT, message, RESET) + // Send the message + target.Msg <- fmt.Sprintf(beep+"[PM from %v] %s%v%s", sender.ColoredName(), privateMessageFormat, message, Reset) logger.Debugf("PM from %v to %v: %v", sender.Name, nick, message) return nil } +// SetMotd sets the Message of the Day (MOTD) func (s *Server) SetMotd(motd string) { s.Lock() s.motd = motd s.Unlock() } +// MotdUnicast sends the MOTD as a SysMsg func (s *Server) MotdUnicast(client *Client) { client.SysMsg("MOTD:\r\n" + ColorString("36", s.motd)) /* a nice cyan color */ } +// MotdBroadcast broadcasts the MOTD func (s *Server) MotdBroadcast(client *Client) { - s.Broadcast(ContinuousFormat(SYSTEM_MESSAGE_FORMAT, fmt.Sprintf(" * New MOTD set by %s.", client.ColoredName())), client) + s.Broadcast(ContinuousFormat(systemMessageFormat, fmt.Sprintf(" * New MOTD set by %s.", client.ColoredName())), client) s.Broadcast(ColorString("36", s.motd), client) } +// Add adds the client to the list of clients func (s *Server) Add(client *Client) { go func() { s.MotdUnicast(client) @@ -158,9 +171,10 @@ func (s *Server) Add(client *Client) { num := len(s.clients) s.Unlock() - s.Broadcast(ContinuousFormat(SYSTEM_MESSAGE_FORMAT, fmt.Sprintf(" * %s joined. (Total connected: %d)", client.ColoredName(), num)), client) + s.Broadcast(ContinuousFormat(systemMessageFormat, fmt.Sprintf(" * %s joined. (Total connected: %d)", client.ColoredName(), num)), client) } +// Remove removes the given client from the list of clients func (s *Server) Remove(client *Client) { s.Lock() delete(s.clients, client.Name) @@ -172,23 +186,24 @@ func (s *Server) Remove(client *Client) { func (s *Server) proposeName(name string) (string, error) { // Assumes caller holds lock. var err error - name = RE_STRIP_TEXT.ReplaceAllString(name, "") + name = reStripText.ReplaceAllString(name, "") - if len(name) > MAX_NAME_LENGTH { - name = name[:MAX_NAME_LENGTH] + if len(name) > maxNameLength { + name = name[:maxNameLength] } else if len(name) == 0 { name = fmt.Sprintf("Guest%d", s.count) } _, collision := s.clients[name] if collision { - err = fmt.Errorf("%s is not available.", name) + err = fmt.Errorf("%s is not available", name) name = fmt.Sprintf("Guest%d", s.count) } return name, err } +// Rename renames the given client (user) func (s *Server) Rename(client *Client, newName string) { s.Lock() @@ -209,10 +224,11 @@ func (s *Server) Rename(client *Client, newName string) { s.SysMsg("%s is now known as %s.", ColorString(client.Color, oldName), ColorString(client.Color, newName)) } +// List lists the clients with the given prefix func (s *Server) List(prefix *string) []string { r := []string{} - for name, _ := range s.clients { + for name := range s.clients { if prefix != nil && !strings.HasPrefix(name, *prefix) { continue } @@ -222,10 +238,12 @@ func (s *Server) List(prefix *string) []string { return r } +// Who returns the client with a given name func (s *Server) Who(name string) *Client { return s.clients[name] } +// Op adds the given fingerprint to the list of admins func (s *Server) Op(fingerprint string) { logger.Infof("Adding admin: %s", fingerprint) s.Lock() @@ -233,6 +251,7 @@ func (s *Server) Op(fingerprint string) { s.Unlock() } +// Whitelist adds the given fingerprint to the whitelist func (s *Server) Whitelist(fingerprint string) { logger.Infof("Adding whitelist: %s", fingerprint) s.Lock() @@ -240,15 +259,18 @@ func (s *Server) Whitelist(fingerprint string) { s.Unlock() } +// Uptime returns the time since the server was started func (s *Server) Uptime() string { return time.Now().Sub(s.started).String() } +// IsOp checks if the given client is Op func (s *Server) IsOp(client *Client) bool { _, r := s.admins[client.Fingerprint()] return r } +// IsWhitelisted checks if the given fingerprint is whitelisted func (s *Server) IsWhitelisted(fingerprint string) bool { /* if no whitelist, anyone is welcome */ if len(s.whitelist) == 0 { @@ -260,6 +282,7 @@ func (s *Server) IsWhitelisted(fingerprint string) bool { return r } +// IsBanned checks if the given fingerprint is banned func (s *Server) IsBanned(fingerprint string) bool { ban, hasBan := s.bannedPk[fingerprint] if !hasBan { @@ -275,6 +298,7 @@ func (s *Server) IsBanned(fingerprint string) bool { return true } +// Ban bans a fingerprint for the given duration func (s *Server) Ban(fingerprint string, duration *time.Duration) { var until *time.Time s.Lock() @@ -286,12 +310,14 @@ func (s *Server) Ban(fingerprint string, duration *time.Duration) { s.Unlock() } +// Unban unbans a banned fingerprint func (s *Server) Unban(fingerprint string) { s.Lock() delete(s.bannedPk, fingerprint) s.Unlock() } +// Start starts the server func (s *Server) Start(laddr string) error { // Once a ServerConfig has been configured, connections can be // accepted. @@ -324,7 +350,7 @@ func (s *Server) Start(laddr string) error { return } - version := RE_STRIP_TEXT.ReplaceAllString(string(sshConn.ClientVersion()), "") + version := reStripText.ReplaceAllString(string(sshConn.ClientVersion()), "") if len(version) > 100 { version = "Evil Jerk with a superlong string" } @@ -346,6 +372,7 @@ func (s *Server) Start(laddr string) error { return nil } +// AutoCompleteFunction handles auto completion of nicks func (s *Server) AutoCompleteFunction(line string, pos int, key rune) (newLine string, newPos int, ok bool) { if key == 9 { shortLine := strings.Split(line[:pos], " ") @@ -372,6 +399,7 @@ func (s *Server) AutoCompleteFunction(line string, pos int, key rune) (newLine s return } +// Stop stops the server func (s *Server) Stop() { for _, client := range s.clients { client.Conn.Close() @@ -380,6 +408,7 @@ func (s *Server) Stop() { close(s.done) } +// Fingerprint returns the fingerprint based on a public key func Fingerprint(k ssh.PublicKey) string { hash := md5.Sum(k.Marshal()) r := fmt.Sprintf("% x", hash) From 86ee6f12c51252db36c00b8a7866626aa3e648c3 Mon Sep 17 00:00:00 2001 From: Peter Hellberg Date: Mon, 15 Dec 2014 01:37:58 +0100 Subject: [PATCH 2/3] Removed type from declaration (it will be inferred from the right-hand side) --- client.go | 2 +- colors.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 5d6e777..ee73d67 100644 --- a/client.go +++ b/client.go @@ -338,7 +338,7 @@ func (c *Client) handleShell(channel ssh.Channel) { if !c.Server.IsOp(c) { c.SysMsg("You're not an admin.") } else { - var split []string = strings.SplitN(line, " ", 2) + var split = strings.SplitN(line, " ", 2) var msg string if len(split) > 1 { msg = split[1] diff --git a/colors.go b/colors.go index 73139e8..c0e5a0f 100644 --- a/colors.go +++ b/colors.go @@ -34,7 +34,7 @@ const ( var colors = []string{"31", "32", "33", "34", "35", "36", "37", "91", "92", "93", "94", "95", "96", "97"} // deColor is used for removing ANSI Escapes -var deColor *regexp.Regexp = regexp.MustCompile("\033\\[[\\d;]+m") +var deColor = regexp.MustCompile("\033\\[[\\d;]+m") // DeColorString removes all color from the given string func DeColorString(s string) string { From ca979b9e72287bd2482651e737291aea50244b3f Mon Sep 17 00:00:00 2001 From: Peter Hellberg Date: Mon, 15 Dec 2014 01:38:26 +0100 Subject: [PATCH 3/3] Changed BannedPk to BannedPK, removed bannedIP --- server.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server.go b/server.go index dd94743..df832b2 100644 --- a/server.go +++ b/server.go @@ -38,8 +38,7 @@ type Server struct { motd string whitelist map[string]struct{} // fingerprint lookup admins map[string]struct{} // fingerprint lookup - bannedPk map[string]*time.Time // fingerprint lookup - bannedIP map[net.Addr]*time.Time + bannedPK map[string]*time.Time // fingerprint lookup started time.Time sync.Mutex } @@ -59,8 +58,7 @@ func NewServer(privateKey []byte) (*Server, error) { motd: "Message of the Day! Modify with /motd", whitelist: map[string]struct{}{}, admins: map[string]struct{}{}, - bannedPk: map[string]*time.Time{}, - bannedIP: map[net.Addr]*time.Time{}, + bannedPK: map[string]*time.Time{}, started: time.Now(), } @@ -284,7 +282,7 @@ func (s *Server) IsWhitelisted(fingerprint string) bool { // IsBanned checks if the given fingerprint is banned func (s *Server) IsBanned(fingerprint string) bool { - ban, hasBan := s.bannedPk[fingerprint] + ban, hasBan := s.bannedPK[fingerprint] if !hasBan { return false } @@ -306,14 +304,14 @@ func (s *Server) Ban(fingerprint string, duration *time.Duration) { when := time.Now().Add(*duration) until = &when } - s.bannedPk[fingerprint] = until + s.bannedPK[fingerprint] = until s.Unlock() } // Unban unbans a banned fingerprint func (s *Server) Unban(fingerprint string) { s.Lock() - delete(s.bannedPk, fingerprint) + delete(s.bannedPK, fingerprint) s.Unlock() }