Refactor server.NewServer

This commit is contained in:
Trevor Slocum 2024-11-20 14:12:33 -08:00
parent bdceb06447
commit 00e4e4da52
8 changed files with 107 additions and 88 deletions

View file

@ -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 <message>`

View file

@ -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)
}

View file

@ -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.

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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])

View file

@ -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)
}