Allow one registration per IP address

This commit is contained in:
Trevor Slocum 2024-09-05 11:38:38 -07:00
parent ef40f1443a
commit c17a214516
12 changed files with 89 additions and 30 deletions

View file

@ -1,6 +1,7 @@
package bgammon
type Client interface {
Address() string
HandleReadWrite()
Write(message []byte)
Terminate(reason string)

View file

@ -22,6 +22,7 @@ func main() {
mailServer string
passwordSalt string
resetSalt string
ipSalt string
verbose bool
debug int
debugCommands bool
@ -48,6 +49,7 @@ func main() {
passwordSalt = os.Getenv("BGAMMON_SALT_PASSWORD")
resetSalt = os.Getenv("BGAMMON_SALT_RESET")
ipSalt = os.Getenv("BGAMMON_SALT_IP")
if rollStatistics {
printRollStatistics()
@ -64,7 +66,7 @@ func main() {
}()
}
s := server.NewServer(tz, dataSource, mailServer, passwordSalt, resetSalt, false, verbose || debug > 0, debugCommands)
s := server.NewServer(tz, dataSource, mailServer, passwordSalt, resetSalt, ipSalt, false, verbose || debug > 0, debugCommands)
if tcpAddress != "" {
s.Listen("tcp", tcpAddress)
}

8
go.mod
View file

@ -11,7 +11,8 @@ require (
github.com/jackc/pgx/v5 v5.6.0
github.com/jlouis/glicko2 v1.0.0
github.com/matcornic/hermes/v2 v2.1.0
golang.org/x/text v0.17.0
golang.org/x/crypto v0.27.0
golang.org/x/text v0.18.0
)
require (
@ -37,7 +38,6 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/vanng822/css v1.0.1 // indirect
github.com/vanng822/go-premailer v1.21.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
)

16
go.sum
View file

@ -103,8 +103,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -117,8 +117,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -135,8 +135,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -152,8 +152,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View file

@ -15,6 +15,7 @@ var _ bgammon.Client = &socketClient{}
type socketClient struct {
conn net.Conn
address string
events chan []byte
commands chan<- []byte
terminated bool
@ -25,12 +26,17 @@ type socketClient struct {
func newSocketClient(conn net.Conn, commands chan<- []byte, events chan []byte, verbose bool) *socketClient {
return &socketClient{
conn: conn,
address: conn.RemoteAddr().String(),
events: events,
commands: commands,
verbose: verbose,
}
}
func (c *socketClient) Address() string {
return c.address
}
func (c *socketClient) HandleReadWrite() {
if c.terminated {
return

View file

@ -21,6 +21,7 @@ var _ bgammon.Client = &webSocketClient{}
type webSocketClient struct {
conn *websocket.Conn
address string
events chan []byte
commands chan<- []byte
terminated bool
@ -34,14 +35,24 @@ func newWebSocketClient(r *http.Request, w http.ResponseWriter, commands chan<-
return nil
}
address := r.Header.Get("X-Forwarded-For")
if address == "" {
address = r.RemoteAddr
}
return &webSocketClient{
conn: conn,
address: address,
events: events,
commands: commands,
verbose: verbose,
}
}
func (c *webSocketClient) Address() string {
return c.address
}
func (c *webSocketClient) HandleReadWrite() {
if c.terminated {
return

View file

@ -30,6 +30,7 @@ const databaseSchema = `
CREATE TABLE account (
id serial PRIMARY KEY,
created bigint NOT NULL,
createdip text NOT NULL,
confirmed bigint NOT NULL DEFAULT 0,
active bigint NOT NULL,
reset bigint NOT NULL DEFAULT 0,
@ -97,7 +98,7 @@ var (
dbLock = &sync.Mutex{}
)
var passwordArgon2id = &argon2id.Params{
var argon2idParameters = &argon2id.Params{
Memory: 128 * 1024,
Iterations: 16,
Parallelism: 4,
@ -151,7 +152,7 @@ func initDB() {
log.Println("Initialized database schema")
}
func registerAccount(passwordSalt string, a *account) error {
func registerAccount(passwordSalt string, a *account, ipHash string) error {
dbLock.Lock()
defer dbLock.Unlock()
@ -178,6 +179,13 @@ func registerAccount(passwordSalt string, a *account) error {
defer tx.Commit(context.Background())
var result int
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE createdip = $1", ipHash).Scan(&result)
if err != nil {
log.Fatal(err)
} else if result > 0 {
return fmt.Errorf("an account has already been registered from your IP address")
}
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE email = $1", bytes.ToLower(bytes.TrimSpace(a.email))).Scan(&result)
if err != nil {
log.Fatal(err)
@ -192,14 +200,14 @@ func registerAccount(passwordSalt string, a *account) error {
return fmt.Errorf("username already in use")
}
passwordHash, err := argon2id.CreateHash(string(a.password)+passwordSalt, passwordArgon2id)
debug.FreeOSMemory() // Password hashing is memory intensive. Return memory to the OS.
passwordHash, err := argon2id.CreateHash(string(a.password)+passwordSalt, argon2idParameters)
debug.FreeOSMemory() // Hashing is memory intensive. Return memory to the OS.
if err != nil {
return err
}
timestamp := time.Now().Unix()
_, err = tx.Exec(context.Background(), "INSERT INTO account (created, active, email, username, password) VALUES ($1, $2, $3, $4, $5)", timestamp, timestamp, bytes.ToLower(bytes.TrimSpace(a.email)), bytes.ToLower(bytes.TrimSpace(a.username)), passwordHash)
_, err = tx.Exec(context.Background(), "INSERT INTO account (created, createdip, active, email, username, password) VALUES ($1, $2, $3, $4, $5, $6)", timestamp, ipHash, timestamp, bytes.ToLower(bytes.TrimSpace(a.email)), bytes.ToLower(bytes.TrimSpace(a.username)), passwordHash)
return err
}
@ -339,8 +347,8 @@ func confirmResetAccount(resetSalt string, passwordSalt string, id int, key stri
newPassword := randomAlphanumeric(7)
passwordHash, err := argon2id.CreateHash(newPassword+passwordSalt, passwordArgon2id)
debug.FreeOSMemory() // Password hashing is memory intensive. Return memory to the OS.
passwordHash, err := argon2id.CreateHash(newPassword+passwordSalt, argon2idParameters)
debug.FreeOSMemory() // Hashing is memory intensive. Return memory to the OS.
if err != nil {
return "", "", err
}
@ -466,7 +474,7 @@ func loginAccount(passwordSalt string, username []byte, password []byte) (*accou
a.muteBearOff = muteBearOff == 1
match, err := argon2id.ComparePasswordAndHash(string(password)+passwordSalt, string(a.password))
debug.FreeOSMemory() // Password hashing is memory intensive. Return memory to the OS.
debug.FreeOSMemory() // Hashing is memory intensive. Return memory to the OS.
if err != nil {
return nil, err
} else if !match {
@ -519,8 +527,8 @@ func setAccountPassword(passwordSalt string, id int, password string) error {
return nil
}
passwordHash, err := argon2id.CreateHash(password+passwordSalt, passwordArgon2id)
debug.FreeOSMemory() // Password hashing is memory intensive. Return memory to the OS.
passwordHash, err := argon2id.CreateHash(password+passwordSalt, argon2idParameters)
debug.FreeOSMemory() // Hashing is memory intensive. Return memory to the OS.
if err != nil {
return err
}

View file

@ -19,7 +19,7 @@ func testDBConnection() error {
func initDB() {
}
func registerAccount(passwordSalt string, a *account) error {
func registerAccount(passwordSalt string, a *account, ipHash string) error {
return nil
}

View file

@ -88,6 +88,7 @@ type server struct {
mailServer string
passwordSalt string
resetSalt string
ipSalt string
tz *time.Location
languageTags []language.Tag
@ -100,7 +101,7 @@ type server struct {
shutdownReason string
}
func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, relayChat bool, verbose bool, allowDebug bool) *server {
func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, ipSalt string, relayChat bool, verbose bool, allowDebug bool) *server {
const bufferSize = 10
s := &server{
newGameIDs: make(chan int),
@ -111,6 +112,7 @@ func NewServer(tz string, dataSource string, mailServer string, passwordSalt str
mailServer: mailServer,
passwordSalt: passwordSalt,
resetSalt: resetSalt,
ipSalt: ipSalt,
relayChat: relayChat,
verbose: verbose,
}

View file

@ -81,7 +81,8 @@ func (s *server) handleFirstCommand(cmd serverCommand, keyword string, params []
username: username,
password: password,
}
err := registerAccount(s.passwordSalt, a)
ipHash := s.hashIP(cmd.client.Address())
err := registerAccount(s.passwordSalt, a, ipHash)
if err != nil {
cmd.client.Terminate(fmt.Sprintf("Failed to register: %s", err))
return
@ -1501,10 +1502,10 @@ COMMANDS:
}
clientGame.Turn = 1
clientGame.Roll1 = 3
clientGame.Roll2 = 3
clientGame.Roll1 = 1
clientGame.Roll2 = 2
clientGame.Roll3 = 0
clientGame.Variant = 0
clientGame.Variant = 1
clientGame.Player1.Entered = true
clientGame.Player2.Entered = true
clientGame.Board = []int8{0, 0, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, -3, 0, 0, -3, -6, -2, 0, 0, 0}

View file

@ -2,8 +2,14 @@
package server
import "log"
import (
"log"
)
func (s *server) Listen(network string, address string) {
log.Fatal("bgammon-server was built without the 'full' tag. Only local connections are possible.")
}
func (s *server) hashIP(address string) string {
return address
}

View file

@ -14,9 +14,14 @@ import (
"code.rocket9labs.com/tslocum/bgammon"
"github.com/gorilla/mux"
"golang.org/x/crypto/sha3"
)
func (s *server) Listen(network string, address string) {
if s.passwordSalt == "" || s.resetSalt == "" || s.ipSalt == "" {
log.Fatal("error: password, reset and ip salts must be configured")
}
if strings.ToLower(network) == "ws" {
go s.listenWebSocket(address)
return
@ -341,3 +346,20 @@ func (s *server) handlePrintWildBGStats(w http.ResponseWriter, r *http.Request)
w.Header().Set("Content-Type", "application/json")
w.Write(s.cachedStats(4))
}
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.ipSalt)
h := make([]byte, 64)
sha3.ShakeSum256(h, buf)
return fmt.Sprintf("%x\n", h)
}