From 00e4e4da52c22bff2c51f366a40cc8161da18450 Mon Sep 17 00:00:00 2001 From: Trevor Slocum Date: Wed, 20 Nov 2024 14:12:33 -0800 Subject: [PATCH] Refactor server.NewServer --- PROTOCOL.md | 2 +- cmd/bgammon-server/main.go | 53 +++++++++-------- command.go | 2 +- pkg/server/client_socket.go | 1 - pkg/server/client_websocket.go | 1 - pkg/server/server.go | 103 +++++++++++++++++++++++---------- pkg/server/server_command.go | 10 ++-- pkg/server/server_full.go | 23 +------- 8 files changed, 107 insertions(+), 88 deletions(-) diff --git a/PROTOCOL.md b/PROTOCOL.md index e920681..0571301 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -105,7 +105,7 @@ formatted responses are more easily parsed by computers. - Aliases: `k` - `rematch` - - Request (or accept) a rematch after a match has been finished. + - Offer (or accept) a rematch after a match has been finished. - Aliases: `rm` - `say ` diff --git a/cmd/bgammon-server/main.go b/cmd/bgammon-server/main.go index afb6591..b487b62 100644 --- a/cmd/bgammon-server/main.go +++ b/cmd/bgammon-server/main.go @@ -14,47 +14,46 @@ import ( ) func main() { + op := &server.Options{} var ( tcpAddress string wsAddress string - tz string - dataSource string - mailServer string - passwordSalt string - resetSalt string - ipSalt string - verbose bool - debug int + debugPort int debugCommands bool rollStatistics bool ) flag.StringVar(&tcpAddress, "tcp", "localhost:1337", "TCP listen address") flag.StringVar(&wsAddress, "ws", "", "WebSocket listen address") - flag.StringVar(&tz, "tz", "", "Time zone used when calculating statistics") - flag.StringVar(&dataSource, "db", "", "Database data source (postgres://username:password@localhost:5432/database_name") - flag.StringVar(&mailServer, "smtp", "", "SMTP server address") - flag.BoolVar(&verbose, "verbose", false, "Print all client messages") - flag.IntVar(&debug, "debug", 0, "print debug information and serve pprof on specified port") + flag.StringVar(&op.TZ, "tz", "", "Time zone used when calculating statistics") + flag.StringVar(&op.DataSource, "db", "", "Database data source (postgres://username:password@localhost:5432/database_name") + flag.StringVar(&op.MailServer, "smtp", "", "SMTP server address") + flag.BoolVar(&op.Verbose, "verbose", false, "Print all client messages") + flag.IntVar(&debugPort, "debug", 0, "print debug information and serve pprof on specified port") flag.BoolVar(&debugCommands, "debug-commands", false, "allow players to use restricted commands") flag.BoolVar(&rollStatistics, "statistics", false, "print dice roll statistics and exit") flag.Parse() - if dataSource == "" { - dataSource = os.Getenv("BGAMMON_DB") + if debugPort > 0 { + op.Debug = true + op.Verbose = true } - if mailServer == "" { - mailServer = os.Getenv("BGAMMON_SMTP") + if op.DataSource == "" { + op.DataSource = os.Getenv("BGAMMON_DB") } - passwordSalt = os.Getenv("BGAMMON_SALT_PASSWORD") - resetSalt = os.Getenv("BGAMMON_SALT_RESET") - ipSalt = os.Getenv("BGAMMON_SALT_IP") + if op.MailServer == "" { + op.MailServer = os.Getenv("BGAMMON_SMTP") + } - certDomain := os.Getenv("BGAMMON_CERT_DOMAIN") - certFolder := os.Getenv("BGAMMON_CERT_FOLDER") - certEmail := os.Getenv("BGAMMON_CERT_EMAIL") - certAddress := os.Getenv("BGAMMON_CERT_ADDRESS") + op.ResetSalt = os.Getenv("BGAMMON_SALT_RESET") + op.PasswordSalt = os.Getenv("BGAMMON_SALT_PASSWORD") + op.IPAddressSalt = os.Getenv("BGAMMON_SALT_IP") + + op.CertDomain = os.Getenv("BGAMMON_CERT_DOMAIN") + op.CertFolder = os.Getenv("BGAMMON_CERT_FOLDER") + op.CertEmail = os.Getenv("BGAMMON_CERT_EMAIL") + op.CertAddress = os.Getenv("BGAMMON_CERT_ADDRESS") if rollStatistics { printRollStatistics() @@ -65,13 +64,13 @@ func main() { log.Fatal("Error: A TCP and/or WebSocket listen address must be specified.") } - if debug > 0 { + if debugPort > 0 { go func() { - log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%d", debug), nil)) + log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%d", debugPort), nil)) }() } - s := server.NewServer(tz, dataSource, mailServer, passwordSalt, resetSalt, ipSalt, certDomain, certFolder, certEmail, certAddress, false, verbose || debug > 0, debugCommands) + s := server.NewServer(op) if tcpAddress != "" { s.Listen("tcp", tcpAddress) } diff --git a/command.go b/command.go index 6aaea3a..a2920c1 100644 --- a/command.go +++ b/command.go @@ -25,7 +25,7 @@ const ( CommandMove = "move" // Move checkers. CommandReset = "reset" // Reset checker movement. CommandOk = "ok" // Confirm checker movement and pass turn to next player. - CommandRematch = "rematch" // Confirm checker movement and pass turn to next player. + CommandRematch = "rematch" // Offer (or accept) a rematch after a match has been finished. CommandFollow = "follow" // Follow a player. CommandUnfollow = "unfollow" // Un-follow a player. CommandBoard = "board" // Print current board state in human-readable form. diff --git a/pkg/server/client_socket.go b/pkg/server/client_socket.go index 7ea24e4..3ae3b35 100644 --- a/pkg/server/client_socket.go +++ b/pkg/server/client_socket.go @@ -26,7 +26,6 @@ type socketClient struct { func newSocketClient(conn net.Conn, commands chan<- []byte, events chan []byte, verbose bool) *socketClient { return &socketClient{ conn: conn, - address: hashIP(conn.RemoteAddr().String()), events: events, commands: commands, verbose: verbose, diff --git a/pkg/server/client_websocket.go b/pkg/server/client_websocket.go index 6bb0bb3..4675823 100644 --- a/pkg/server/client_websocket.go +++ b/pkg/server/client_websocket.go @@ -38,7 +38,6 @@ func newWebSocketClient(r *http.Request, w http.ResponseWriter, commands chan<- return &webSocketClient{ conn: conn, - address: hashIP(r.RemoteAddr), events: events, commands: commands, verbose: verbose, diff --git a/pkg/server/server.go b/pkg/server/server.go index a5e2076..62107e1 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -18,6 +18,7 @@ import ( "code.rocket9labs.com/tslocum/bgammon" "code.rocket9labs.com/tslocum/gotext" + "golang.org/x/crypto/sha3" "golang.org/x/text/language" ) @@ -34,10 +35,6 @@ var ( alphaNumericUnderscore = regexp.MustCompile(`^[A-Za-z0-9_]+$`) ) -var allowDebugCommands bool - -var ipSalt string - //go:embed locales var assetFS embed.FS @@ -82,9 +79,10 @@ type server struct { sortedCommands []string - mailServer string - passwordSalt string - resetSalt string + mailServer string + resetSalt string + passwordSalt string + ipAddressSalt string tz *time.Location languageTags []language.Tag @@ -97,28 +95,53 @@ type server struct { relayChat bool // Chats are not relayed normally. This option is only used by local servers. verbose bool + debug bool // Allow users to run debug commands. shutdownTime time.Time shutdownReason string } -func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, ipAddressSalt string, certDomain string, certFolder string, certEmail string, certAddress string, relayChat bool, verbose bool, allowDebug bool) *server { +type Options struct { + TZ string + DataSource string + MailServer string + + RelayChat bool + Verbose bool + Debug bool + + CertDomain string + CertFolder string + CertEmail string + CertAddress string + + ResetSalt string + PasswordSalt string + IPAddressSalt string +} + +func NewServer(op *Options) *server { + if op == nil { + op = &Options{} + } const bufferSize = 10 s := &server{ - newGameIDs: make(chan int), - newClientIDs: make(chan int), - commands: make(chan serverCommand, bufferSize), - welcome: []byte("hello Welcome to bgammon.org! Please log in by sending the 'login' command. You may specify a username, otherwise you will be assigned a random username. If you specify a username, you may also specify a password. Have fun!"), - defcon: 5, - mailServer: mailServer, - passwordSalt: passwordSalt, - resetSalt: resetSalt, - certDomain: certDomain, - certFolder: certFolder, - certEmail: certEmail, - certAddress: certAddress, - relayChat: relayChat, - verbose: verbose, + newGameIDs: make(chan int), + newClientIDs: make(chan int), + commands: make(chan serverCommand, bufferSize), + welcome: []byte("hello Welcome to bgammon.org! Please log in by sending the 'login' command. You may specify a username, otherwise you will be assigned a random username. If you specify a username, you may also specify a password. Have fun!"), + defcon: 5, + mailServer: op.MailServer, + resetSalt: op.ResetSalt, + passwordSalt: op.PasswordSalt, + ipAddressSalt: op.IPAddressSalt, + certDomain: op.CertDomain, + certFolder: op.CertFolder, + certEmail: op.CertEmail, + certAddress: op.CertAddress, + relayChat: op.RelayChat, + verbose: op.Verbose, + debug: op.Debug, } s.loadLocales() @@ -127,20 +150,18 @@ func NewServer(tz string, dataSource string, mailServer string, passwordSalt str } sort.Slice(s.sortedCommands, func(i, j int) bool { return s.sortedCommands[i] < s.sortedCommands[j] }) - if tz != "" { + if op.TZ != "" { var err error - s.tz, err = time.LoadLocation(tz) + s.tz, err = time.LoadLocation(op.TZ) if err != nil { - log.Fatalf("failed to parse timezone %s: %s", tz, err) + log.Fatalf("failed to parse timezone %s: %s", op.TZ, err) } } else { s.tz = time.UTC } - ipSalt = ipAddressSalt - - if dataSource != "" { - err := connectDB(dataSource) + if op.DataSource != "" { + err := connectDB(op.DataSource) if err != nil { log.Fatalf("failed to connect to database: %s", err) } @@ -155,8 +176,6 @@ func NewServer(tz string, dataSource string, mailServer string, passwordSalt str log.Println("Connected to database successfully") } - allowDebugCommands = allowDebug - go s.handleNewGameIDs() go s.handleNewClientIDs() go s.handleCommands() @@ -408,6 +427,9 @@ func (s *server) handleConnection(conn net.Conn) { now := time.Now().Unix() + sc := newSocketClient(conn, commands, events, s.verbose) + sc.address = s.hashIP(conn.RemoteAddr().String()) + c := &serverClient{ id: <-s.newClientIDs, language: "bgammon-en", @@ -415,7 +437,7 @@ func (s *server) handleConnection(conn net.Conn) { connected: now, active: now, commands: commands, - Client: newSocketClient(conn, commands, events, s.verbose), + Client: sc, } s.sendWelcome(c) s.handleClient(c) @@ -530,6 +552,23 @@ func (s *server) gameByClient(c *serverClient) *serverGame { return nil } +func (s *server) hashIP(address string) string { + leftBracket, rightBracket := strings.IndexByte(address, '['), strings.IndexByte(address, ']') + if leftBracket != -1 && rightBracket != -1 && rightBracket > leftBracket { + address = address[1:rightBracket] + } else if strings.IndexByte(address, '.') != -1 { + colon := strings.IndexByte(address, ':') + if colon != -1 { + address = address[:colon] + } + } + + buf := []byte(address + s.ipAddressSalt) + h := make([]byte, 64) + sha3.ShakeSum256(h, buf) + return fmt.Sprintf("%x\n", h) +} + func (s *server) handleShutdown() { var mins time.Duration var minutes int diff --git a/pkg/server/server_command.go b/pkg/server/server_command.go index dc76fbb..f35c341 100644 --- a/pkg/server/server_command.go +++ b/pkg/server/server_command.go @@ -1554,7 +1554,7 @@ COMMANDS: isIP := bytes.ContainsRune(params[0], '.') || bytes.ContainsRune(params[0], ':') if isIP { - ip := hashIP(string(params[0])) + ip := s.hashIP(string(params[0])) err := addBan(ip, 0, cmd.client.accountID, reason) if err != nil { cmd.client.sendNotice("Failed to add ban: " + err.Error()) @@ -1626,7 +1626,7 @@ COMMANDS: isIP := bytes.ContainsRune(params[0], '.') || bytes.ContainsRune(params[0], ':') if isIP { - err := deleteBan(hashIP(string(params[0])), 0) + err := deleteBan(s.hashIP(string(params[0])), 0) if err != nil { cmd.client.sendNotice("Failed to remove ban: " + err.Error()) continue @@ -1667,7 +1667,7 @@ COMMANDS: s.shutdown(time.Duration(minutes)*time.Minute, string(bytes.Join(params[1:], []byte(" ")))) case "endgame": - if !allowDebugCommands { + if !s.debug { cmd.client.sendNotice(gotext.GetD(cmd.client.language, "You are not allowed to use that command.")) continue } else if clientGame == nil { @@ -1680,9 +1680,9 @@ COMMANDS: clientGame.Roll2 = 5 clientGame.Roll3 = 0 clientGame.Variant = bgammon.VariantAceyDeucey - clientGame.Player1.Entered = false + clientGame.Player1.Entered = true clientGame.Player2.Entered = true - clientGame.Board = []int8{1, 2, 1, 3, 2, 2, 2, 0, 0, 0, -3, -7, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, -3, 0, 0, -2} + clientGame.Board = []int8{0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0} log.Println(clientGame.Board[0:28]) diff --git a/pkg/server/server_full.go b/pkg/server/server_full.go index 3e8391e..af10e16 100644 --- a/pkg/server/server_full.go +++ b/pkg/server/server_full.go @@ -16,12 +16,11 @@ import ( "code.rocket9labs.com/tslocum/bgammon" "github.com/gorilla/mux" "golang.org/x/crypto/acme/autocert" - "golang.org/x/crypto/sha3" ) func (s *server) Listen(network string, address string) { - if s.passwordSalt == "" || s.resetSalt == "" || ipSalt == "" { - log.Fatal("error: password, reset and ip salts must be configured") + if s.passwordSalt == "" || s.resetSalt == "" || s.ipAddressSalt == "" { + log.Fatal("error: password, reset and ip salts must be configured before listening for remote clients") } if strings.ToLower(network) == "ws" { @@ -125,6 +124,7 @@ func (s *server) handleWebSocket(w http.ResponseWriter, r *http.Request) { if wsClient == nil { return } + wsClient.address = s.hashIP(r.RemoteAddr) now := time.Now().Unix() @@ -400,20 +400,3 @@ func (s *server) handlePrintWildBGStats(w http.ResponseWriter, r *http.Request) w.Header().Set("Content-Type", "application/json") w.Write(s.cachedStats(4)) } - -func hashIP(address string) string { - leftBracket, rightBracket := strings.IndexByte(address, '['), strings.IndexByte(address, ']') - if leftBracket != -1 && rightBracket != -1 && rightBracket > leftBracket { - address = address[1:rightBracket] - } else if strings.IndexByte(address, '.') != -1 { - colon := strings.IndexByte(address, ':') - if colon != -1 { - address = address[:colon] - } - } - - buf := []byte(address + ipSalt) - h := make([]byte, 64) - sha3.ShakeSum256(h, buf) - return fmt.Sprintf("%x\n", h) -}