-
-
Notifications
You must be signed in to change notification settings - Fork 21
/
server.go
468 lines (387 loc) · 15.3 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
// Package gopher is used to start and change the core settings for the Gopher Game Server. The
// type ServerSettings contains all the parameters for changing the core settings. You can either
// pass a ServerSettings when calling Server.Start() or nil if you want to use the default server
// settings.
package gopher
import (
"context"
"encoding/json"
"fmt"
"github.com/hewiefreeman/GopherGameServer/actions"
"github.com/hewiefreeman/GopherGameServer/core"
"github.com/hewiefreeman/GopherGameServer/database"
"io/ioutil"
"net/http"
"os"
"strconv"
"time"
)
/////////// TO DOs:
/////////// - Make authentication for GopherDB
/////////// - Admin tools
/////////// - More useful command-line macros
// ServerSettings are the core settings for the Gopher Game Server. You must fill one of these out to customize
// the server's functionality to your liking.
type ServerSettings struct {
ServerName string // The server's name. Used for the server's ownership of private Rooms. (Required)
MaxConnections int // The maximum amount of concurrent connections the server will accept. Setting this to 0 means infinite.
HostName string // Server's host name. Use 'https://' for TLS connections. (ex: 'https://example.com') (Required)
HostAlias string // Server's host alias name. Use 'https://' for TLS connections. (ex: 'https://www.example.com')
IP string // Server's IP address. (Required)
Port int // Server's port. (Required)
TLS bool // Enables TLS/SSL connections.
CertFile string // SSL/TLS certificate file location (starting from system's root folder). (Required for TLS)
PrivKeyFile string // SSL/TLS private key file location (starting from system's root folder). (Required for TLS)
OriginOnly bool // When enabled, the server declines connections made from outside the origin server (Admin logins always check origin). IMPORTANT: Enable this for web apps and LAN servers.
MultiConnect bool // Enables multiple connections under the same User. When enabled, will override KickDupOnLogin's functionality.
MaxUserConns uint8 // Overrides the default (255) of maximum simultaneous connections on a single User
KickDupOnLogin bool // When enabled, a logged in User will be disconnected from service when another User logs in with the same name.
UserRoomControl bool // Enables Users to create Rooms, invite/uninvite(AKA revoke) other Users to their owned private rooms, and destroy their owned rooms.
RoomDeleteOnLeave bool // When enabled, Rooms created by a User will be deleted when the owner leaves. WARNING: If disabled, you must remember to at some point delete the rooms created by Users, or they will pile up endlessly!
EnableSqlFeatures bool // Enables the built-in SQL User authentication and friending. NOTE: It is HIGHLY recommended to use TLS over an SSL/HTTPS connection when using the SQL features. Otherwise, sensitive User information can be compromised with network "snooping" (AKA "sniffing").
SqlIP string // SQL Database IP address. (Required for SQL features)
SqlPort int // SQL Database port. (Required for SQL features)
SqlProtocol string // The protocol to use while comminicating with the MySQL database. Most use either 'udp' or 'tcp'. (Required for SQL features)
SqlUser string // SQL user name (Required for SQL features)
SqlPassword string // SQL user password (Required for SQL features)
SqlDatabase string // SQL database name (Required for SQL features)
EncryptionCost int // The amount of encryption iterations the server will run when storing and checking passwords. The higher the number, the longer encryptions take, but are more secure. Default is 4, range is 4-31.
CustomLoginColumn string // The custom AccountInfoColumn you wish to use for logging in instead of the default name column.
RememberMe bool // Enables the "Remember Me" login feature. You can read more about this in project's wiki.
EnableRecovery bool // Enables the recovery of all Rooms, their settings, and their variables on start-up after terminating the server.
RecoveryLocation string // The folder location (starting from system's root folder) where you would like to store the recovery data. (Required for recovery)
AdminLogin string // The login name for the Admin Tools (Required for Admin Tools)
AdminPassword string // The password for the Admin Tools (Required for Admin Tools)
}
type serverRestore struct {
R map[string]core.RoomRecoveryState
}
var (
httpServer *http.Server
settings *ServerSettings
serverStarted bool = false
serverPaused bool = false
serverStopping bool = false
serverEndChan chan error = make(chan error)
startCallback func()
pauseCallback func()
stopCallback func()
resumeCallback func()
clientConnectCallback func(*http.ResponseWriter, *http.Request) bool
//SERVER VERSION NUMBER
version string = "1.0-BETA.2"
)
//////////////////////////////////////////////////////////////////////////////////////////////////////
// Server start-up ///////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// Start will start the server. Call with a pointer to your `ServerSettings` (or nil for defaults) to start the server. The default
// settings are for local testing ONLY. There are security-related options in `ServerSettings`
// for SSL/TLS, connection origin testing, administrator tools, and more. It's highly recommended to look into
// all `ServerSettings` options to tune the server for your desired functionality and security needs.
//
// This function will block the thread that it is ran on until the server either errors, or is manually shut-down. To run code after the
// server starts/stops/pauses/etc, use the provided server callback setter functions.
func Start(s *ServerSettings) {
if serverStarted || serverPaused {
return
}
serverStarted = true
fmt.Println(" _______ __\n | _ |.-----..-----.| |--..-----..----.\n |. |___||. _ ||. _ ||. ||. -__||. _|\n |. | ||:. . ||:. __||: |: ||: ||: |\n |: | |'-----'|: | '--'--''-----''--'\n |::.. . | '--' - Game Server -\n '-------'\n\n ")
fmt.Println("Starting server...")
// Set server settings
if s != nil {
if !s.verify() {
return
}
settings = s
} else {
// Default localhost settings
fmt.Println("Using default settings...")
settings = &ServerSettings{
ServerName: "!server!",
MaxConnections: 0,
HostName: "localhost",
HostAlias: "localhost",
IP: "localhost",
Port: 8080,
TLS: false,
CertFile: "",
PrivKeyFile: "",
OriginOnly: false,
MultiConnect: false,
KickDupOnLogin: false,
UserRoomControl: true,
RoomDeleteOnLeave: true,
EnableSqlFeatures: false,
SqlIP: "localhost",
SqlPort: 3306,
SqlProtocol: "tcp",
SqlUser: "user",
SqlPassword: "password",
SqlDatabase: "database",
EncryptionCost: 4,
CustomLoginColumn: "",
RememberMe: false,
EnableRecovery: false,
RecoveryLocation: "C:/",
AdminLogin: "admin",
AdminPassword: "password"}
}
// Update package settings
core.SettingsSet((*settings).KickDupOnLogin, (*settings).ServerName, (*settings).RoomDeleteOnLeave, (*settings).EnableSqlFeatures,
(*settings).RememberMe, (*settings).MultiConnect, (*settings).MaxUserConns)
// Notify packages of server start
core.SetServerStarted(true)
actions.SetServerStarted(true)
database.SetServerStarted(true)
// Start database
if (*settings).EnableSqlFeatures {
fmt.Println("Initializing database...")
dbErr := database.Init((*settings).SqlUser, (*settings).SqlPassword, (*settings).SqlDatabase,
(*settings).SqlProtocol, (*settings).SqlIP, (*settings).SqlPort, (*settings).EncryptionCost,
(*settings).RememberMe, (*settings).CustomLoginColumn)
if dbErr != nil {
fmt.Println("Database error:", dbErr.Error())
fmt.Println("Shutting down...")
return
}
fmt.Println("Database initialized")
}
// Recover state
if settings.EnableRecovery {
recoverState()
}
// Start socket listener
if settings.TLS {
httpServer = makeServer("/wss", settings.TLS)
} else {
httpServer = makeServer("/ws", settings.TLS)
}
// Run callback
if startCallback != nil {
startCallback()
}
// Start macro listener
go macroListener()
fmt.Println("Startup complete")
// Wait for server shutdown
doneErr := <-serverEndChan
if doneErr != http.ErrServerClosed {
fmt.Println("Fatal server error:", doneErr.Error())
if !serverStopping {
fmt.Println("Disconnecting users...")
// Pause server
core.Pause()
actions.Pause()
database.Pause()
// Save state
if settings.EnableRecovery {
saveState()
}
}
}
fmt.Println("Server shut-down completed")
if stopCallback != nil {
stopCallback()
}
}
func (settings *ServerSettings) verify() bool {
if settings.ServerName == "" {
fmt.Println("ServerName in ServerSettings is required. Shutting down...")
return false
} else if settings.HostName == "" || settings.IP == "" || settings.Port < 1 {
fmt.Println("HostName, IP, and Port in ServerSettings are required. Shutting down...")
return false
} else if settings.TLS == true && (settings.CertFile == "" || settings.PrivKeyFile == "") {
fmt.Println("CertFile and PrivKeyFile in ServerSettings are required for a TLS connection. Shutting down...")
return false
} else if settings.EnableSqlFeatures == true && (settings.SqlIP == "" || settings.SqlPort < 1 || settings.SqlProtocol == "" ||
settings.SqlUser == "" || settings.SqlPassword == "" || settings.SqlDatabase == "") {
fmt.Println("SqlIP, SqlPort, SqlProtocol, SqlUser, SqlPassword, and SqlDatabase in ServerSettings are required for the SQL features. Shutting down...")
return false
} else if settings.EnableRecovery == true && settings.RecoveryLocation == "" {
fmt.Println("RecoveryLocation in ServerSettings is required for server recovery. Shutting down...")
return false
} else if settings.EnableRecovery {
// Check if invalid file location
if _, err := os.Stat(settings.RecoveryLocation); err != nil {
fmt.Println("RecoveryLocation error:", err)
fmt.Println("Shutting down...")
return false
}
var d []byte
if err := ioutil.WriteFile(settings.RecoveryLocation+"/test.txt", d, 0644); err != nil {
fmt.Println("RecoveryLocation error:", err)
fmt.Println("Shutting down...")
return false
}
os.Remove(settings.RecoveryLocation + "/test.txt")
} else if settings.AdminLogin == "" || settings.AdminPassword == "" {
fmt.Println("AdminLogin and AdminPassword in ServerSettings are required. Shutting down...")
return false
}
return true
}
func makeServer(handleDir string, tls bool) *http.Server {
server := &http.Server{Addr: settings.IP + ":" + strconv.Itoa(settings.Port)}
http.HandleFunc(handleDir, socketInitializer)
if tls {
go func() {
err := server.ListenAndServeTLS(settings.CertFile, settings.PrivKeyFile)
serverEndChan <- err
}()
} else {
go func() {
err := server.ListenAndServe()
serverEndChan <- err
}()
}
//
return server
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// Server actions ////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// Pause will log all Users off and prevent anyone from logging in. All rooms and their variables created by the server will remain in memory.
// Same goes for rooms created by Users unless RoomDeleteOnLeave in ServerSettings is set to true.
func Pause() {
if !serverPaused {
serverPaused = true
fmt.Println("Pausing server...")
core.Pause()
actions.Pause()
database.Pause()
// Run callback
if pauseCallback != nil {
pauseCallback()
}
fmt.Println("Server paused")
serverStarted = false
}
}
// Resume will allow Users to login again after pausing the server.
func Resume() {
if serverPaused {
serverStarted = true
fmt.Println("Resuming server...")
core.Resume()
actions.Resume()
database.Resume()
// Run callback
if resumeCallback != nil {
resumeCallback()
}
fmt.Println("Server resumed")
serverPaused = false
}
}
// ShutDown will log all Users off, save the state of the server if EnableRecovery in ServerSettings is set to true, then shut the server down.
func ShutDown() error {
if !serverStopping {
serverStopping = true
fmt.Println("Disconnecting users...")
// Pause server
core.Pause()
actions.Pause()
database.Pause()
// Save state
if settings.EnableRecovery {
saveState()
}
// Shut server down
fmt.Println("Shutting server down...")
shutdownErr := httpServer.Shutdown(context.Background())
if shutdownErr != http.ErrServerClosed {
return shutdownErr
}
}
//
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// Saving and recovery ///////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
func saveState() {
fmt.Println("Saving server state...")
saveErr := writeState(getState(), settings.RecoveryLocation)
if saveErr != nil {
fmt.Println("Error saving state:", saveErr)
return
}
fmt.Println("Save state successful")
}
func writeState(stateObj serverRestore, saveFolder string) error {
state, err := json.Marshal(stateObj)
if err != nil {
return err
}
err = ioutil.WriteFile(saveFolder+"/Gopher Recovery - "+time.Now().Format("2006-01-02 15-04-05")+".grf", state, 0644)
if err != nil {
return err
}
return nil
}
func getState() serverRestore {
return serverRestore{
R: core.GetRoomsState(),
}
}
func recoverState() {
fmt.Println("Recovering previous state...")
// Get last recovery file
files, fileErr := ioutil.ReadDir(settings.RecoveryLocation)
if fileErr != nil {
fmt.Println("Error recovering state:", fileErr)
return
}
var newestFile string
var newestTime int64
for _, f := range files {
if len(f.Name()) < 19 || f.Name()[0:15] != "Gopher Recovery" {
continue
}
fi, err := os.Stat(settings.RecoveryLocation + "/" + f.Name())
if err != nil {
fmt.Println("Error recovering state:", err)
return
}
currTime := fi.ModTime().Unix()
if currTime > newestTime {
newestTime = currTime
newestFile = f.Name()
}
}
// Read file
r, err := ioutil.ReadFile(settings.RecoveryLocation + "/" + newestFile)
if err != nil {
fmt.Println("Error recovering state:", err)
return
}
// Convert JSON
var recovery serverRestore
if err = json.Unmarshal(r, &recovery); err != nil {
fmt.Println("Error recovering state:", err)
return
}
if recovery.R == nil || len(recovery.R) == 0 {
fmt.Println("No rooms to restore!")
return
}
// Recover rooms
for name, val := range recovery.R {
room, roomErr := core.NewRoom(name, val.T, val.P, val.M, val.O)
if roomErr != nil {
fmt.Println("Error recovering room '"+name+"':", roomErr)
continue
}
for _, userName := range val.I {
invErr := room.AddInvite(userName)
if invErr != nil {
fmt.Println("Error inviting '"+userName+"' to the room '"+name+"':", invErr)
}
}
room.SetVariables(val.V)
}
//
fmt.Println("State recovery successful")
}