1 package server
2
3 import (
4 "bufio"
5 "bytes"
6 "crypto/rand"
7 "encoding/base64"
8 "fmt"
9 "log"
10 "math/big"
11 "net"
12 "net/http"
13 "os"
14 "os/exec"
15 "regexp"
16 "strconv"
17 "strings"
18 "sync"
19 "time"
20
21 "code.rocket9labs.com/tslocum/bgammon"
22 )
23
24 const clientTimeout = 40 * time.Second
25
26 const inactiveLimit = 600
27
28 var allowDebugCommands bool
29
30 var (
31 onlyNumbers = regexp.MustCompile(`^[0-9]+$`)
32 guestName = regexp.MustCompile(`^guest[0-9]+$`)
33 alphaNumericUnderscore = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
34 )
35
36 type serverCommand struct {
37 client *serverClient
38 command []byte
39 }
40
41 type server struct {
42 clients []*serverClient
43 games []*serverGame
44 listeners []net.Listener
45 newGameIDs chan int
46 newClientIDs chan int
47 commands chan serverCommand
48 welcome []byte
49
50 gamesLock sync.RWMutex
51 clientsLock sync.Mutex
52
53 gamesCache []byte
54 gamesCacheTime time.Time
55 gamesCacheLock sync.Mutex
56
57 statsCache [4][]byte
58 statsCacheTime time.Time
59 statsCacheLock sync.Mutex
60
61 leaderboardCache [12][]byte
62 leaderboardCacheTime time.Time
63 leaderboardCacheLock sync.Mutex
64
65 mailServer string
66 passwordSalt string
67 resetSalt string
68
69 tz *time.Location
70
71 relayChat bool
72 verbose bool
73 }
74
75 func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, relayChat bool, verbose bool, allowDebug bool) *server {
76 const bufferSize = 10
77 s := &server{
78 newGameIDs: make(chan int),
79 newClientIDs: make(chan int),
80 commands: make(chan serverCommand, bufferSize),
81 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!"),
82 mailServer: mailServer,
83 passwordSalt: passwordSalt,
84 resetSalt: resetSalt,
85 relayChat: relayChat,
86 verbose: verbose,
87 }
88
89 if tz != "" {
90 var err error
91 s.tz, err = time.LoadLocation(tz)
92 if err != nil {
93 log.Fatalf("failed to parse timezone %s: %s", tz, err)
94 }
95 } else {
96 s.tz = time.UTC
97 }
98
99 if dataSource != "" {
100 err := connectDB(dataSource)
101 if err != nil {
102 log.Fatalf("failed to connect to database: %s", err)
103 }
104
105 err = testDBConnection()
106 if err != nil {
107 log.Fatalf("failed to test database connection: %s", err)
108 }
109
110 initDB()
111
112 log.Println("Connected to database successfully")
113 }
114
115 allowDebugCommands = allowDebug
116
117
135
136 go s.handleNewGameIDs()
137 go s.handleNewClientIDs()
138 go s.handleCommands()
139 go s.handleGames()
140 return s
141 }
142
143 func (s *server) ListenLocal() chan net.Conn {
144 conns := make(chan net.Conn)
145 go s.handleLocal(conns)
146 return conns
147 }
148
149 func (s *server) handleLocal(conns chan net.Conn) {
150 for {
151 local, remote := net.Pipe()
152
153 conns <- local
154 go s.handleConnection(remote)
155 }
156 }
157
158 func (s *server) Listen(network string, address string) {
159 if strings.ToLower(network) == "ws" {
160 go s.listenWebSocket(address)
161 return
162 }
163
164 log.Printf("Listening for %s connections on %s...", strings.ToUpper(network), address)
165 listener, err := net.Listen(network, address)
166 if err != nil {
167 log.Fatalf("failed to listen on %s: %s", address, err)
168 }
169 go s.handleListener(listener)
170 s.listeners = append(s.listeners, listener)
171 }
172
173 func (s *server) handleListener(listener net.Listener) {
174 for {
175 conn, err := listener.Accept()
176 if err != nil {
177 log.Fatalf("failed to accept connection: %s", err)
178 }
179 go s.handleConnection(conn)
180 }
181 }
182
183 func (s *server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
184 const bufferSize = 8
185 commands := make(chan []byte, bufferSize)
186 events := make(chan []byte, bufferSize)
187
188 wsClient := newWebSocketClient(r, w, commands, events, s.verbose)
189 if wsClient == nil {
190 return
191 }
192
193 now := time.Now().Unix()
194
195 c := &serverClient{
196 id: <-s.newClientIDs,
197 accountID: -1,
198 connected: now,
199 active: now,
200 commands: commands,
201 Client: wsClient,
202 }
203 s.handleClient(c)
204 }
205
206 func (s *server) nameAllowed(username []byte) bool {
207 return !guestName.Match(username)
208 }
209
210 func (s *server) clientByUsername(username []byte) *serverClient {
211 lower := bytes.ToLower(username)
212 for _, c := range s.clients {
213 if bytes.Equal(bytes.ToLower(c.name), lower) {
214 return c
215 }
216 }
217 return nil
218 }
219
220 func (s *server) addClient(c *serverClient) {
221 s.clientsLock.Lock()
222 defer s.clientsLock.Unlock()
223
224 s.clients = append(s.clients, c)
225 }
226
227 func (s *server) removeClient(c *serverClient) {
228 g := s.gameByClient(c)
229 if g != nil {
230 g.removeClient(c)
231 }
232 c.Terminate("")
233
234 close(c.commands)
235
236 s.clientsLock.Lock()
237 defer s.clientsLock.Unlock()
238
239 for i, sc := range s.clients {
240 if sc == c {
241 s.clients = append(s.clients[:i], s.clients[i+1:]...)
242 return
243 }
244 }
245 }
246
247 func (s *server) handleGames() {
248 t := time.NewTicker(time.Minute)
249 for range t.C {
250 s.gamesLock.Lock()
251
252 i := 0
253 for _, g := range s.games {
254 if !g.PartialHandled() && g.Player1.Rating != 0 && g.Player2.Rating != 0 {
255 partialTurn := g.PartialTurn()
256 if partialTurn != 0 {
257 total := g.PartialTime()
258 switch partialTurn {
259 case 1:
260 total += g.Player1.Inactive
261 case 2:
262 total += g.Player2.Inactive
263 }
264 if total >= inactiveLimit {
265 g.inactive = partialTurn
266 g.SetPartialHandled(true)
267 if !g.terminated() {
268 var player *serverClient
269 var opponent *serverClient
270 switch partialTurn {
271 case 1:
272 player = g.client1
273 opponent = g.client2
274 case 2:
275 player = g.client2
276 opponent = g.client1
277 }
278 if player != nil {
279 player.sendNotice("You have been inactive for more than ten minutes. If your opponent leaves the match they will receive a win.")
280 }
281 if opponent != nil {
282 opponent.sendNotice("Your opponent has been inactive for more than ten minutes. You may continue playing or leave the match at any time and receive a win.")
283 }
284 }
285 }
286 }
287 }
288
289 if !g.terminated() {
290 s.games[i] = g
291 i++
292 } else if g.Winner == 0 && (g.inactive != 0 || g.forefeit != 0) {
293 if g.inactive != 0 {
294 g.Winner = 1
295 if g.inactive == 1 {
296 g.Winner = 2
297 }
298 } else {
299 g.Winner = 1
300 if g.forefeit == 1 {
301 g.Winner = 2
302 }
303 }
304 err := recordMatchResult(g, matchTypeCasual)
305 if err != nil {
306 log.Fatalf("failed to record match result: %s", err)
307 }
308 }
309 }
310 for j := i; j < len(s.games); j++ {
311 s.games[j] = nil
312 }
313 s.games = s.games[:i]
314
315 s.gamesLock.Unlock()
316 }
317 }
318
319 func (s *server) handleClient(c *serverClient) {
320 s.addClient(c)
321
322 log.Printf("Client %s connected", c.label())
323
324 go s.handlePingClient(c)
325 go s.handleClientCommands(c)
326
327 c.HandleReadWrite()
328
329
330 s.removeClient(c)
331
332 log.Printf("Client %s disconnected", c.label())
333 }
334
335 func (s *server) handleConnection(conn net.Conn) {
336 const bufferSize = 8
337 commands := make(chan []byte, bufferSize)
338 events := make(chan []byte, bufferSize)
339
340 now := time.Now().Unix()
341
342 c := &serverClient{
343 id: <-s.newClientIDs,
344 accountID: -1,
345 connected: now,
346 active: now,
347 commands: commands,
348 Client: newSocketClient(conn, commands, events, s.verbose),
349 }
350 s.sendHello(c)
351 s.handleClient(c)
352 }
353
354 func (s *server) handlePingClient(c *serverClient) {
355
356 t := time.NewTicker(30 * time.Second)
357 for {
358 <-t.C
359
360 if c.Terminated() {
361 t.Stop()
362 return
363 }
364
365 if len(c.name) == 0 {
366 c.Terminate("User did not send login command within 30 seconds.")
367 t.Stop()
368 return
369 }
370
371 c.lastPing = time.Now().Unix()
372 c.sendEvent(&bgammon.EventPing{
373 Message: fmt.Sprintf("%d", c.lastPing),
374 })
375 }
376 }
377
378 func (s *server) handleClientCommands(c *serverClient) {
379 var command []byte
380 for command = range c.commands {
381 s.commands <- serverCommand{
382 client: c,
383 command: command,
384 }
385 }
386 }
387
388 func (s *server) handleNewGameIDs() {
389 gameID := 1
390 for {
391 s.newGameIDs <- gameID
392 gameID++
393 }
394 }
395
396 func (s *server) handleNewClientIDs() {
397 clientID := 1
398 for {
399 s.newClientIDs <- clientID
400 clientID++
401 }
402 }
403
404
405 func (s *server) randomUsername() []byte {
406 for {
407 name := []byte(fmt.Sprintf("Guest_%d", 100+RandInt(900)))
408
409 if s.clientByUsername(name) == nil {
410 return name
411 }
412 }
413 }
414
415 func (s *server) sendHello(c *serverClient) {
416 if c.json {
417 return
418 }
419 c.Write(s.welcome)
420 }
421
422 func (s *server) gameByClient(c *serverClient) *serverGame {
423 s.gamesLock.RLock()
424 defer s.gamesLock.RUnlock()
425
426 for _, g := range s.games {
427 if g.client1 == c || g.client2 == c {
428 return g
429 }
430 for _, spec := range g.spectators {
431 if spec == c {
432 return g
433 }
434 }
435 }
436 return nil
437 }
438
439
440 func (s *server) Analyze(g *bgammon.Game) {
441 cmd := exec.Command("gnubg", "--tty")
442 stdin, err := cmd.StdinPipe()
443 if err != nil {
444 log.Fatal(err)
445 }
446 stdout, err := cmd.StdoutPipe()
447 if err != nil {
448 log.Fatal(err)
449 }
450 stderr, err := cmd.StderrPipe()
451 if err != nil {
452 log.Fatal(err)
453 }
454 err = cmd.Start()
455 if err != nil {
456 log.Fatal(err)
457 }
458
459 go func() {
460 scanner := bufio.NewScanner(stdout)
461 for scanner.Scan() {
462 log.Println("STDOUT", string(scanner.Bytes()))
463 }
464 }()
465
466 go func() {
467 scanner := bufio.NewScanner(stderr)
468 for scanner.Scan() {
469 log.Println("STDERR", string(scanner.Bytes()))
470 }
471 }()
472
473 stdin.Write([]byte(fmt.Sprintf("new game\nset board %s\nanalyze game\n", gnubgPosition(g))))
474
475 time.Sleep(2 * time.Second)
476 os.Exit(0)
477 }
478
479 func RandInt(max int) int {
480 i, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
481 if err != nil {
482 panic(err)
483 }
484 return int(i.Int64())
485 }
486
487 func gnubgPosition(g *bgammon.Game) string {
488 var opponent int8 = 2
489 start := 0
490 end := 25
491 boardStart := 1
492 boardEnd := 24
493 delta := 1
494 playerBarSpace := bgammon.SpaceBarPlayer
495 opponentBarSpace := bgammon.SpaceBarOpponent
496 switch g.Turn {
497 case 1:
498 case 2:
499 opponent = 1
500 start = 25
501 end = 0
502 boardStart = 24
503 boardEnd = 1
504 delta = -1
505 playerBarSpace = bgammon.SpaceBarOpponent
506 opponentBarSpace = bgammon.SpaceBarPlayer
507 default:
508 log.Fatalf("failed to analyze game: zero turn")
509 }
510
511 var buf []byte
512 for space := boardStart; space != end; space += delta {
513 playerCheckers := bgammon.PlayerCheckers(g.Board[space], g.Turn)
514 for i := int8(0); i < playerCheckers; i++ {
515 buf = append(buf, '1')
516 }
517 buf = append(buf, '0')
518 }
519 playerCheckers := bgammon.PlayerCheckers(g.Board[playerBarSpace], g.Turn)
520 for i := int8(0); i < playerCheckers; i++ {
521 buf = append(buf, '1')
522 }
523 buf = append(buf, '0')
524
525 for space := boardEnd; space != start; space -= delta {
526 opponentCheckers := bgammon.PlayerCheckers(g.Board[space], opponent)
527 for i := int8(0); i < opponentCheckers; i++ {
528 buf = append(buf, '1')
529 }
530 buf = append(buf, '0')
531 }
532 opponentCheckers := bgammon.PlayerCheckers(g.Board[opponentBarSpace], opponent)
533 for i := int8(0); i < opponentCheckers; i++ {
534 buf = append(buf, '1')
535 }
536 buf = append(buf, '0')
537
538 for i := len(buf); i < 80; i++ {
539 buf = append(buf, '0')
540 }
541
542 var out []byte
543 for i := 0; i < len(buf); i += 8 {
544 s := reverseString(string(buf[i : i+8]))
545 v, err := strconv.ParseUint(s, 2, 8)
546 if err != nil {
547 panic(err)
548 }
549 out = append(out, byte(v))
550 }
551
552 position := base64.StdEncoding.EncodeToString(out)
553 if len(position) == 0 {
554 return ""
555 }
556 for position[len(position)-1] == '=' {
557 position = position[:len(position)-1]
558 }
559 return position
560 }
561
562 func reverseString(s string) string {
563 runes := []rune(s)
564 for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
565 runes[i], runes[j] = runes[j], runes[i]
566 }
567 return string(runes)
568 }
569
570 type ratingPlayer struct {
571 r float64
572 rd float64
573 sigma float64
574 outcome float64
575 }
576
577 func (p ratingPlayer) R() float64 {
578 return p.r
579 }
580
581 func (p ratingPlayer) RD() float64 {
582 return p.rd
583 }
584
585 func (p ratingPlayer) Sigma() float64 {
586 return p.sigma
587 }
588
589 func (p ratingPlayer) SJ() float64 {
590 return p.outcome
591 }
592
View as plain text