Send password reset email

This commit is contained in:
Trevor Slocum 2023-12-13 19:04:02 -08:00
parent c7d4c1825f
commit c793dc4aeb
8 changed files with 465 additions and 41 deletions

View file

@ -16,7 +16,7 @@ Players always perceive games from the perspective of player number 1 (black).
### Client commands
Clients must send a register command or login command before sending any other commands.
Clients must send a register command, reset command or login command before sending any other commands.
- `register <email> <username> <password>`
- Register an account. A valid email address must be provided.
@ -29,6 +29,10 @@ formatted responses are more easily parsed by computers.
- The name of the client must be specified.
- Aliases: `rj`
- `resetpassword <email>`
- Request a password reset link via email.
- This command always terminates the client with the message "resetpasswordok", even if an account is not found.
- `login [username] [password]`
- Log in. A random username is assigned when none is provided.
- Usernames must contain at least one non-numeric character.

View file

@ -17,6 +17,9 @@ func main() {
wsAddress string
tz string
dataSource string
mailServer string
passwordSalt string
resetSalt string
debug int
debugCommands bool
rollStatistics bool
@ -25,6 +28,7 @@ func main() {
flag.StringVar(&wsAddress, "ws", "localhost:1338", "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.IntVar(&debug, "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")
@ -34,6 +38,13 @@ func main() {
dataSource = os.Getenv("BGAMMON_DB")
}
if mailServer == "" {
mailServer = os.Getenv("BGAMMON_SMTP")
}
passwordSalt = os.Getenv("BGAMMON_SALT_PASSWORD")
resetSalt = os.Getenv("BGAMMON_SALT_RESET")
if rollStatistics {
printRollStatistics()
return
@ -49,7 +60,7 @@ func main() {
}()
}
s := server.NewServer(tz, dataSource, false, debugCommands)
s := server.NewServer(tz, dataSource, mailServer, passwordSalt, resetSalt, false, debugCommands)
if tcpAddress != "" {
s.Listen("tcp", tcpAddress)
}

View file

@ -3,27 +3,28 @@ package bgammon
type Command string
const (
CommandLogin = "login" // Log in with username and password, or as a guest.
CommandLoginJSON = "loginjson" // Log in with username and password, or as a guest, and enable JSON messages.
CommandRegister = "register" // Register an account.
CommandRegisterJSON = "registerjson" // Register an account and enable JSON messages.
CommandHelp = "help" // Print help information.
CommandJSON = "json" // Enable or disable JSON formatted messages.
CommandSay = "say" // Send chat message.
CommandList = "list" // List available matches.
CommandCreate = "create" // Create match.
CommandJoin = "join" // Join match.
CommandLeave = "leave" // Leave match.
CommandDouble = "double" // Offer double to opponent.
CommandResign = "resign" // Decline double offer and resign game.
CommandRoll = "roll" // Roll dice.
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.
CommandBoard = "board" // Print current board state in human-readable form.
CommandPong = "pong" // Response to server ping.
CommandDisconnect = "disconnect" // Disconnect from server.
CommandLogin = "login" // Log in with username and password, or as a guest.
CommandLoginJSON = "loginjson" // Log in with username and password, or as a guest, and enable JSON messages.
CommandRegister = "register" // Register an account.
CommandRegisterJSON = "registerjson" // Register an account and enable JSON messages.
CommandResetPassword = "resetpassword" // Request password reset link via email.
CommandHelp = "help" // Print help information.
CommandJSON = "json" // Enable or disable JSON formatted messages.
CommandSay = "say" // Send chat message.
CommandList = "list" // List available matches.
CommandCreate = "create" // Create match.
CommandJoin = "join" // Join match.
CommandLeave = "leave" // Leave match.
CommandDouble = "double" // Offer double to opponent.
CommandResign = "resign" // Decline double offer and resign game.
CommandRoll = "roll" // Roll dice.
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.
CommandBoard = "board" // Print current board state in human-readable form.
CommandPong = "pong" // Response to server ping.
CommandDisconnect = "disconnect" // Disconnect from server.
)
type EventType string

24
go.mod
View file

@ -1,19 +1,41 @@
module code.rocket9labs.com/tslocum/bgammon
go 1.20
go 1.17
require (
github.com/alexedwards/argon2id v1.0.0
github.com/gobwas/ws v1.3.1
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.5.1
github.com/matcornic/hermes/v2 v2.1.0
)
require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/vanng822/css v1.0.1 // indirect
github.com/vanng822/go-premailer v1.20.2 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

95
go.sum
View file

@ -1,13 +1,48 @@
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
@ -15,36 +50,91 @@ github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZ
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/unrolled/render v1.0.3/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4=
github.com/vanng822/go-premailer v1.20.2 h1:vKs4VdtfXDqL7IXC2pkiBObc1bXM9bYH3Wa+wYw2DnI=
github.com/vanng822/go-premailer v1.20.2/go.mod h1:RAxbRFp6M/B171gsKu8dsyq+Y5NGsUUvYfg+WQWusbE=
github.com/vanng822/r2router v0.0.0-20150523112421-1023140a4f30/go.mod h1:1BVq8p2jVr55Ost2PkZWDrG86PiJ/0lxqcXoAcGxvWU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
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=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
@ -52,10 +142,12 @@ golang.org/x/sys v0.15.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=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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=
@ -68,5 +160,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -5,23 +5,34 @@ package server
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"log"
"math/rand"
"mime/multipart"
"net/smtp"
"net/textproto"
"strconv"
"strings"
"time"
"code.rocket9labs.com/tslocum/bgammon"
"github.com/alexedwards/argon2id"
"github.com/jackc/pgx/v5"
"github.com/matcornic/hermes/v2"
)
const databaseSchema = `
CREATE TABLE account (
id serial PRIMARY KEY,
created bigint NOT NULL,
active bigint NOT NULL,
email text NOT NULL,
username text NOT NULL,
password text NOT NULL
id serial PRIMARY KEY,
created bigint NOT NULL,
confirmed bigint NOT NULL DEFAULT 0,
active bigint NOT NULL,
reset bigint NOT NULL DEFAULT 0,
email text NOT NULL,
username text NOT NULL,
password text NOT NULL
);
CREATE TABLE game (
id serial PRIMARY KEY,
@ -142,11 +153,149 @@ func registerAccount(a *account) error {
return err
}
func resetAccount(mailServer string, resetSalt string, email []byte) error {
if db == nil {
return nil
} else if len(bytes.TrimSpace(email)) == 0 {
return fmt.Errorf("please enter an email address")
}
tx, err := begin()
if err != nil {
return err
}
defer tx.Commit(context.Background())
var result int
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE email = $1", bytes.ToLower(bytes.TrimSpace(email))).Scan(&result)
if err != nil {
return err
} else if result == 0 {
return nil
}
var (
id int
reset int64
accountEmail []byte
passwordHash []byte
)
err = tx.QueryRow(context.Background(), "SELECT id, reset, email, password FROM account WHERE email = $1", bytes.ToLower(bytes.TrimSpace(email))).Scan(&id, &reset, &accountEmail, &passwordHash)
if err != nil {
return err
} else if id == 0 || len(passwordHash) == 0 {
return nil
}
const resetTimeout = 86400 // 24 hours.
if time.Now().Unix()-reset >= resetTimeout {
timestamp := time.Now().Unix()
h := sha256.New()
h.Write([]byte(fmt.Sprintf("%d", timestamp) + resetSalt))
hash := fmt.Sprintf("%x", h.Sum(nil))[0:16]
emailConfig := hermes.Hermes{
Product: hermes.Product{
Name: "https://bgammon.org",
Link: " ",
Copyright: " ",
},
}
resetEmail := hermes.Email{
Body: hermes.Body{
Greeting: "Hello",
Intros: []string{
"You are receiving this email because you (or someone else) requested to reset your bgammon.org password.",
},
Actions: []hermes.Action{
{
Instructions: "Click to reset your password:",
Button: hermes.Button{
Color: "#DC4D2F",
Text: "Reset your password",
Link: "https://bgammon.org/reset/" + strconv.Itoa(id) + "/" + hash,
},
},
},
Outros: []string{
"If you did not request to reset your bgammon.org password, no further action is required on your part.",
},
Signature: "Ciao",
},
}
emailPlain, err := emailConfig.GeneratePlainText(resetEmail)
if err != nil {
return nil
}
emailPlain = strings.ReplaceAll(emailPlain, "https://bgammon.org -", "https://bgammon.org")
emailHTML, err := emailConfig.GenerateHTML(resetEmail)
if err != nil {
return nil
}
if sendEmail(mailServer, string(accountEmail), "Reset your bgammon.org password", emailPlain, emailHTML) {
_, err = tx.Exec(context.Background(), "UPDATE account SET reset = $1 WHERE id = $2", timestamp, id)
}
return err
}
return nil
}
func confirmResetAccount(resetSalt string, id int, key string) (string, error) {
if db == nil {
return "", nil
} else if id == 0 {
return "", fmt.Errorf("no id provided")
} else if len(strings.TrimSpace(key)) == 0 {
return "", fmt.Errorf("no reset key provided")
}
tx, err := begin()
if err != nil {
return "", err
}
defer tx.Commit(context.Background())
var result int
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE id = $1 AND reset != 0", id).Scan(&result)
if err != nil {
return "", err
} else if result == 0 {
return "", nil
}
var reset int
err = tx.QueryRow(context.Background(), "SELECT reset FROM account WHERE id = $1", id).Scan(&reset)
if err != nil {
return "", err
}
h := sha256.New()
h.Write([]byte(fmt.Sprintf("%d", reset) + resetSalt))
hash := fmt.Sprintf("%x", h.Sum(nil))[0:16]
if key != hash {
return "", nil
}
newPassword := randomAlphanumeric(7)
passwordHash, err := argon2id.CreateHash(newPassword, passwordArgon2id)
if err != nil {
return "", err
}
_, err = tx.Exec(context.Background(), "UPDATE account SET password = $1, reset = reset - 1 WHERE id = $2", passwordHash, id)
return newPassword, err
}
func loginAccount(username []byte, password []byte) (*account, error) {
if db == nil {
return nil, nil
} else if len(bytes.TrimSpace(username)) == 0 {
return nil, fmt.Errorf("please enter an email address")
return nil, fmt.Errorf("please enter a username")
} else if len(bytes.TrimSpace(password)) == 0 {
return nil, fmt.Errorf("please enter a password")
}
@ -325,3 +474,92 @@ func botStats(name string, tz *time.Location) (*botStatsResult, error) {
func midnight(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
func sendEmail(mailServer string, emailAddress string, emailSubject string, emailPlain string, emailHTML string) bool {
mixedContent := &bytes.Buffer{}
mixedWriter := multipart.NewWriter(mixedContent)
var newBoundary = "RELATED-" + mixedWriter.Boundary()
mixedWriter.SetBoundary(first70("MIXED-" + mixedWriter.Boundary()))
relatedWriter, newBoundary := nestedMultipart(mixedWriter, "multipart/related", newBoundary)
altWriter, newBoundary := nestedMultipart(relatedWriter, "multipart/alternative", "ALTERNATIVE-"+newBoundary)
var childContent io.Writer
childContent, _ = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/plain"}})
childContent.Write([]byte(emailPlain))
childContent, _ = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/html"}})
childContent.Write([]byte(emailHTML))
altWriter.Close()
relatedWriter.Close()
mixedWriter.Close()
if mailServer == "" {
fmt.Print(`From: bgammon.org <noreply@bgammon.org>
To: <` + emailAddress + `>
Subject: ` + emailSubject + `
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=`)
fmt.Print(mixedWriter.Boundary(), "\n\n")
fmt.Println(mixedContent.String())
return true
}
c, err := smtp.Dial(mailServer)
if err != nil {
return false
}
defer c.Close()
c.Mail("noreply@bgammon.org")
c.Rcpt(emailAddress)
wc, err := c.Data()
if err != nil {
return false
}
defer wc.Close()
fmt.Fprint(wc, `From: bgammon.org <noreply@bgammon.org>
To: `+emailAddress+`
Subject: `+emailSubject+`
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=`)
fmt.Fprint(wc, mixedWriter.Boundary(), "\n\n")
fmt.Fprintln(wc, mixedContent.String())
return true
}
func nestedMultipart(enclosingWriter *multipart.Writer, contentType, boundary string) (nestedWriter *multipart.Writer, newBoundary string) {
var contentBuffer io.Writer
var err error
boundary = first70(boundary)
contentWithBoundary := contentType + "; boundary=\"" + boundary + "\""
contentBuffer, err = enclosingWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {contentWithBoundary}})
if err != nil {
log.Fatal(err)
}
nestedWriter = multipart.NewWriter(contentBuffer)
newBoundary = nestedWriter.Boundary()
nestedWriter.SetBoundary(boundary)
return
}
func first70(str string) string {
if len(str) > 70 {
return string(str[0:69])
}
return str
}
var letters = []rune("abcdefghkmnpqrstwxyzABCDEFGHJKMNPQRSTWXYZ23456789")
func randomAlphanumeric(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

View file

@ -23,6 +23,14 @@ func registerAccount(a *account) error {
return nil
}
func resetAccount(mailServer string, resetSalt string, email []byte) error {
return nil
}
func confirmResetAccount(resetSalt string, id int, key string) (string, error) {
return "", nil
}
func loginAccount(username []byte, password []byte) (*account, error) {
return nil, nil
}

View file

@ -16,6 +16,7 @@ import (
"time"
"code.rocket9labs.com/tslocum/bgammon"
"github.com/gorilla/mux"
)
const clientTimeout = 40 * time.Second
@ -49,18 +50,25 @@ type server struct {
gamesCacheTime time.Time
gamesCacheLock sync.Mutex
mailServer string
passwordSalt string
resetSalt string
tz *time.Location
relayChat bool // Chats are not relayed normally. This option is only used by local servers.
}
func NewServer(tz string, dataSource string, relayChat bool, allowDebug bool) *server {
func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, relayChat bool, allowDebug bool) *server {
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!"),
mailServer: mailServer,
passwordSalt: passwordSalt,
resetSalt: resetSalt,
relayChat: relayChat,
}
@ -132,6 +140,27 @@ func (s *server) cachedMatches() []byte {
return s.gamesCache
}
func (s *server) handleResetPassword(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil || id <= 0 {
return
}
key := vars["key"]
newPassword, err := confirmResetAccount(s.resetSalt, id, key)
if err != nil {
log.Printf("failed to reset password: %s", err)
}
w.Header().Set("Content-Type", "text/html")
if err != nil || newPassword == "" {
w.Write([]byte(`<!DOCTYPE html><html><body><h1>Invalid or expired password reset link.</h1></body></html>`))
return
}
w.Write([]byte(`<!DOCTYPE html><html><body><h1>Your bgammon.org password has been reset.</h1>Your new password is <b>` + newPassword + `</b></body></html>`))
}
func (s *server) handleListMatches(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(s.cachedMatches())
@ -205,14 +234,15 @@ func (s *server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
func (s *server) listenWebSocket(address string) {
log.Printf("Listening for WebSocket connections on %s...", address)
mux := http.NewServeMux()
mux.HandleFunc("/matches", s.handleListMatches)
mux.HandleFunc("/stats", s.handlePrintStats)
mux.HandleFunc("/stats-tabula", s.handlePrintTabulaStats)
mux.HandleFunc("/stats-wildbg", s.handlePrintWildBGStats)
mux.HandleFunc("/", s.handleWebSocket)
m := mux.NewRouter()
m.HandleFunc("/reset/{id:[0-9]+}/{key:[A-Za-z0-9]+}", s.handleResetPassword)
m.HandleFunc("/matches", s.handleListMatches)
m.HandleFunc("/stats", s.handlePrintStats)
m.HandleFunc("/stats-tabula", s.handlePrintTabulaStats)
m.HandleFunc("/stats-wildbg", s.handlePrintWildBGStats)
m.HandleFunc("/", s.handleWebSocket)
err := http.ListenAndServe(address, mux)
err := http.ListenAndServe(address, m)
log.Fatalf("failed to listen on %s: %s", address, err)
}
@ -467,6 +497,21 @@ COMMANDS:
// Require users to send login command first.
if cmd.client.account == -1 {
resetCommand := keyword == bgammon.CommandResetPassword
if resetCommand {
if len(params) > 0 {
email := bytes.ToLower(bytes.TrimSpace(params[0]))
if len(email) > 0 {
err := resetAccount(s.mailServer, s.resetSalt, email)
if err != nil {
log.Fatalf("failed to reset password: %s", err)
}
}
}
cmd.client.Terminate("resetpasswordok")
continue
}
loginCommand := keyword == bgammon.CommandLogin || keyword == bgammon.CommandLoginJSON || keyword == "lj"
registerCommand := keyword == bgammon.CommandRegister || keyword == bgammon.CommandRegisterJSON || keyword == "rj"
if loginCommand || registerCommand {
@ -508,7 +553,7 @@ COMMANDS:
a := &account{
email: email,
username: username,
password: password,
password: append(password, []byte(s.passwordSalt)...),
}
err := registerAccount(a)
if err != nil {
@ -556,7 +601,7 @@ COMMANDS:
}
if len(password) > 0 {
a, err := loginAccount(username, password)
a, err := loginAccount(username, append(password, []byte(s.passwordSalt)...))
if err != nil {
cmd.client.Terminate(fmt.Sprintf("Failed to log in: %s", err))
continue