1 // Package gameserver implements a simple messaging server to be used with several built in games. 2 package gameserver 3 4 import ( 5 "encoding/json" 6 "errors" 7 "net/http" 8 "strconv" 9 "sync" 10 "time" 11 12 "golang.org/x/net/websocket" 13 "vimagination.zapto.org/httpgzip" 14 "vimagination.zapto.org/jsonrpc" 15 ) 16 17 // New create a new gameserver mux, with the given dataDir used for data required by the games 18 func New(dataDir http.FileSystem) *http.ServeMux { 19 m := http.NewServeMux() 20 m.Handle("/", index) 21 m.Handle("/data/", http.StripPrefix("/data/", httpgzip.FileServer(dataDir))) 22 m.Handle("/socket", websocket.Handler(newServer().ServeConn)) 23 return m 24 } 25 26 var noData = json.RawMessage{'{', '}'} 27 28 type conns map[*conn]struct{} 29 30 type server struct { 31 mu sync.RWMutex 32 rooms map[string]*room 33 conns conns 34 } 35 36 func newServer() *server { 37 return &server{ 38 rooms: map[string]*room{ 39 "default": newRoom("default", nil), 40 }, 41 conns: make(conns), 42 } 43 } 44 45 func (s *server) ServeConn(wconn *websocket.Conn) { 46 c := &conn{ 47 server: s, 48 } 49 s.mu.Lock() 50 s.conns[c] = struct{}{} 51 s.mu.Unlock() 52 c.rpc = jsonrpc.New(wconn, c) 53 c.rpc.Handle() 54 s.mu.Lock() 55 if c.room != nil { 56 if c.room.leave(c) { 57 s.removeRoom(c.room.Name) 58 } 59 } 60 delete(s.conns, c) 61 s.mu.Unlock() 62 } 63 64 func (s *server) removeRoom(name string) { 65 delete(s.rooms, name) 66 broadcast(s.conns, broadcastRoomRemove, strconv.AppendQuote(json.RawMessage{}, name)) 67 } 68 69 type conn struct { 70 rpc *jsonrpc.Server 71 server *server 72 73 mu sync.RWMutex 74 name string 75 room *room 76 } 77 78 type room struct { 79 Name string 80 81 mu sync.RWMutex 82 status json.RawMessage 83 admin *conn 84 users conns 85 names map[string]*conn 86 } 87 88 func newRoom(name string, admin *conn) *room { 89 names := make(map[string]*conn) 90 if admin != nil { 91 names[admin.name] = admin 92 } 93 return &room{ 94 Name: name, 95 status: json.RawMessage{'{', '}'}, 96 admin: admin, 97 users: make(conns), 98 names: names, 99 } 100 } 101 102 func (r *room) join(conn *conn) (json.RawMessage, error) { 103 r.mu.Lock() 104 defer r.mu.Unlock() 105 if _, ok := r.names[conn.name]; ok { 106 return nil, errNameExists 107 } 108 var ( 109 adminName string 110 data = json.RawMessage{'{', '"', 'u', 's', 'e', 'r', 's', '"', ':', '['} 111 first = true 112 ) 113 for name, c := range r.names { 114 if r.admin == c { 115 adminName = name 116 continue 117 } 118 if first { 119 first = false 120 } else { 121 data = append(data, ',') 122 } 123 data = strconv.AppendQuote(data, name) 124 } 125 data = append(append(append(strconv.AppendQuote(append(data, "],\"admin\":"...), adminName), ",\"data\":"...), r.status...), '}') 126 r.names[conn.name] = conn 127 r.users[r.admin] = struct{}{} 128 broadcast(r.users, broadcastUserJoin, strconv.AppendQuote(json.RawMessage{}, conn.name)) 129 delete(r.users, r.admin) 130 r.users[conn] = struct{}{} 131 return data, nil 132 } 133 134 func (r *room) spectate(conn *conn) json.RawMessage { 135 r.mu.Lock() 136 defer r.mu.Unlock() 137 r.users[conn] = struct{}{} 138 return r.status 139 } 140 141 func (r *room) leave(conn *conn) bool { 142 r.mu.Lock() 143 defer r.mu.Unlock() 144 delete(r.names, conn.name) 145 delete(r.users, conn) 146 if r.admin == conn { 147 r.admin = nil 148 r.status = noData 149 broadcast(r.users, broadcastAdminNone, noData) 150 } else if conn.name != "" { 151 r.users[r.admin] = struct{}{} 152 broadcast(r.users, broadcastUserLeave, strconv.AppendQuote(json.RawMessage{}, conn.name)) 153 delete(r.users, r.admin) 154 } 155 return len(r.names) == 0 && r.Name != "default" 156 } 157 158 func (r *room) promote(conn *conn) error { 159 r.mu.Lock() 160 defer r.mu.Unlock() 161 if r.admin != nil { 162 return errAdminExists 163 } 164 if _, ok := r.users[conn]; !ok || conn.name == "" { 165 return errNotUser 166 } 167 delete(r.users, conn) 168 r.admin = conn 169 broadcast(r.users, broadcastAdmin, strconv.AppendQuote(json.RawMessage{}, conn.name)) 170 return nil 171 } 172 173 type roomUser struct { 174 Room string `json:"room"` 175 User string `json:"user"` 176 } 177 178 func (c *conn) HandleRPC(method string, data json.RawMessage) (interface{}, error) { 179 if len(data) > 0x100000 { 180 return nil, errMessageTooBig 181 } 182 switch method { 183 case "time": 184 return time.Now().Unix(), nil 185 case "listRooms": 186 rooms := json.RawMessage{'['} 187 c.server.mu.RLock() 188 defer c.server.mu.RUnlock() 189 first := true 190 for room := range c.server.rooms { 191 if first { 192 first = false 193 } else { 194 rooms = append(rooms, ',') 195 } 196 rooms = strconv.AppendQuote(rooms, room) 197 } 198 return append(rooms, ']'), nil 199 case "addRoom": 200 var names roomUser 201 if err := json.Unmarshal(data, &names); err != nil { 202 return nil, err 203 } 204 if len(names.Room) > 100 || len(names.User) > 100 { 205 return nil, errNameTooLong 206 } 207 c.mu.Lock() 208 defer c.mu.Unlock() 209 c.server.mu.Lock() 210 defer c.server.mu.Unlock() 211 if c.room != nil { 212 if c.room.leave(c) { 213 c.server.removeRoom(c.room.Name) 214 } 215 } 216 c.room = nil 217 c.name = "" 218 if _, ok := c.server.rooms[names.Room]; ok { 219 return nil, errRoomExists 220 } 221 c.name = names.User 222 c.room = newRoom(names.Room, c) 223 c.server.rooms[names.Room] = c.room 224 broadcast(c.server.conns, broadcastRoomAdd, strconv.AppendQuote(data[:0], names.Room)) 225 return nil, nil 226 case "joinRoom": 227 var names roomUser 228 if err := json.Unmarshal(data, &names); err != nil { 229 return nil, err 230 } 231 if len(names.User) > 100 { 232 return nil, errNameTooLong 233 } 234 c.server.mu.Lock() 235 defer c.server.mu.Unlock() 236 c.mu.Lock() 237 defer c.mu.Unlock() 238 if c.room != nil { 239 if c.room.leave(c) { 240 c.server.removeRoom(c.room.Name) 241 } 242 } 243 c.room = nil 244 room, ok := c.server.rooms[names.Room] 245 if !ok { 246 return nil, errUnknownRoom 247 } 248 c.name = names.User 249 var ( 250 roomJSON json.RawMessage 251 err error 252 ) 253 if c.name == "" { 254 roomJSON = room.spectate(c) 255 } else { 256 roomJSON, err = room.join(c) 257 } 258 if err != nil { 259 return nil, err 260 } 261 c.room = room 262 return roomJSON, nil 263 case "leaveRoom": 264 c.server.mu.Lock() 265 defer c.server.mu.Unlock() 266 c.mu.Lock() 267 defer c.mu.Unlock() 268 if c.room == nil { 269 return nil, errNotInRoom 270 } 271 if c.room.leave(c) { 272 c.server.removeRoom(c.room.Name) 273 } 274 c.room = nil 275 c.name = "" 276 return nil, nil 277 case "adminRoom": 278 c.mu.Lock() 279 defer c.mu.Unlock() 280 if c.room == nil { 281 return nil, errNotInRoom 282 } 283 if err := c.room.promote(c); err != nil { 284 return nil, err 285 } 286 return nil, nil 287 case "message": 288 if len(data) == 0 { 289 data = noData 290 } 291 c.mu.RLock() 292 defer c.mu.RUnlock() 293 if c.room == nil { 294 return nil, errNotInRoom 295 } 296 c.room.mu.Lock() 297 defer c.room.mu.Unlock() 298 if c.room.admin == c { 299 var message struct { 300 Game string `json:"game"` 301 To string `json:"to"` 302 } 303 if err := json.Unmarshal(data, &message); err != nil { 304 return nil, err 305 } 306 if len(message.To) > 0 { 307 userConn, ok := c.room.names[message.To] 308 if !ok { 309 return nil, errNoUser 310 } 311 go userConn.rpc.SendData(buildBroadcast(broadcastMessageUser, data)) 312 } else if len(message.Game) > 0 { 313 c.room.status = data 314 broadcast(c.room.users, broadcastMessageRoom, data) 315 } 316 } else if _, ok := c.room.names[c.name]; ok { 317 go c.room.admin.rpc.SendData(buildBroadcast(broadcastMessageAdmin, append(append(append(strconv.AppendQuote(append(json.RawMessage{}, "{\"from\":"...), c.name), ",\"data\":"...), data...), '}'))) 318 } else { 319 return nil, errNotUser 320 } 321 return nil, nil 322 } 323 return nil, errUnknownEndpoint 324 } 325 326 const ( 327 broadcastRoomAdd int8 = -1 - iota 328 broadcastRoomRemove 329 broadcastAdminNone 330 broadcastAdmin 331 broadcastUserJoin 332 broadcastUserLeave 333 broadcastMessageAdmin 334 broadcastMessageUser 335 broadcastMessageRoom 336 ) 337 338 const broadcastStart = "{\"id\":-0,\"result\":" 339 340 func buildBroadcast(id int8, data json.RawMessage) json.RawMessage { 341 l := len(broadcastStart) + len(data) + 1 342 dat := make(json.RawMessage, l) 343 copy(dat, broadcastStart) 344 copy(dat[len(broadcastStart):], data) 345 id = -id 346 dat[7] = byte('0' + id%10) 347 dat[l-1] = '}' 348 return dat 349 } 350 351 func broadcast(conns conns, broadcastID int8, message json.RawMessage) { 352 if len(conns) == 0 { 353 return 354 } 355 dat := buildBroadcast(broadcastID, message) 356 for conn := range conns { 357 if conn != nil { 358 go conn.rpc.SendData(dat) 359 } 360 } 361 } 362 363 var ( 364 errUnknownEndpoint = errors.New("unknown endpoint") 365 errRoomExists = errors.New("room exists") 366 errNameExists = errors.New("name exists") 367 errUnknownRoom = errors.New("unknown room") 368 errAdminExists = errors.New("admin exists") 369 errNotUser = errors.New("not user") 370 errNotInRoom = errors.New("not in room") 371 errNoUser = errors.New("no user") 372 errMessageTooBig = errors.New("message too big") 373 errNameTooLong = errors.New("name too long") 374 ) 375