gamekey 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ require "securerandom"
2
+ require "digest"
3
+ require "uri"
4
+
5
+ #
6
+ # Defines all CONSTANTS for Gamekey.
7
+ #
8
+ module Defaults
9
+
10
+ # Default port number
11
+ PORT = 8080
12
+
13
+ # Default storage file
14
+ STORAGE = "gamekey.json"
15
+
16
+ # Initial storage content
17
+ DB = {
18
+ service: "Gamekey",
19
+ storage: SecureRandom.uuid,
20
+ version: "0.0.2",
21
+ users: [],
22
+ games: [],
23
+ gamestates: []
24
+ }
25
+
26
+ # Used crpyto hash function
27
+ CRYPTOHASH = Digest::SHA256.new
28
+
29
+ # Default test host of gamekey service
30
+ TESTHOST = "http://localhost:#{PORT}"
31
+
32
+ # Regular Expression to validate Email adresses
33
+ VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
34
+
35
+ # Regular Expression to validate http and https uris adresses
36
+ VALID_URL_REGEX = /\A#{URI::regexp(['http', 'https'])}\z/
37
+
38
+ # A Test hash for testing gamestate handling
39
+ TESTHASH = {
40
+ 'this' => 'is',
41
+ 'a' => ['simple', 'test']
42
+ }
43
+
44
+ # A Test list for testing gamestate handling
45
+ TESTLIST = ['This', 'is', 'a', 'Test']
46
+
47
+ end
@@ -0,0 +1,632 @@
1
+ require "gamekey/version"
2
+ require "sinatra/base"
3
+ require 'sinatra/cross_origin'
4
+
5
+ require "defaults"
6
+ require "auth"
7
+ require "json"
8
+ require "securerandom"
9
+ require "digest"
10
+
11
+ require 'colorize'
12
+
13
+ #
14
+ # This class defines the REST API for the gamekey service.
15
+ #
16
+ class GamekeyService
17
+
18
+ # Path to storage file
19
+ attr_reader :storage
20
+
21
+ # Port
22
+ attr_reader :port
23
+
24
+ # Hash of the in memory database
25
+ attr_reader :memory
26
+
27
+ # Constructor to create a Gamekey service
28
+ #
29
+ def initialize(storage: Defaults::STORAGE, port: Defaults::PORT)
30
+ @storage = storage
31
+ @port = port
32
+
33
+ File.open(storage, "w") { |file| file.write(Defaults::DB.to_json) } unless File.exist?(storage)
34
+ content = File.read(storage)
35
+
36
+ # print(content)
37
+ @memory = JSON.parse(content)
38
+ end
39
+
40
+ # Gets user hash by id from memory.
41
+ # return nil, if id is not present
42
+ #
43
+ def get_user_by_id(id)
44
+ @memory['users'].select { |user| user['id'] == id }.first
45
+ end
46
+
47
+ # Gets user hash by name from memory.
48
+ # return nil, if name is not present
49
+ #
50
+ def get_user_by_name(name)
51
+ @memory['users'].select { |user| user['name'] == name }.first
52
+ end
53
+
54
+ # Gets game hash by id from memory.
55
+ # return nil, if id is not present
56
+ #
57
+ def get_game_by_id(id)
58
+ @memory['games'].select { |game| game['id'] == id }.first
59
+ end
60
+
61
+ # Defines the CORS enabled REST API of the Gamekey service.
62
+ # Following resources are managed:
63
+ #
64
+ # - User
65
+ # - Game
66
+ # - Gamestate
67
+ #
68
+ def api()
69
+
70
+ memory = @memory
71
+ storage = @storage
72
+ port = @port
73
+ service = self
74
+
75
+ Sinatra.new do
76
+
77
+ register Sinatra::CrossOrigin
78
+
79
+ set :bind, "0.0.0.0"
80
+ set :port, port
81
+ enable :cross_origin
82
+ set :allow_origin, :any
83
+ set :allow_methods, [:get, :post, :options, :delete, :put]
84
+
85
+ #
86
+ # Standard 404 message
87
+ #
88
+ not_found do
89
+ "not found"
90
+ end
91
+
92
+ options "*" do
93
+ response.headers["Allow"] = "HEAD,GET,PUT,POST,DELETE,OPTIONS"
94
+ response.headers["Access-Control-Allow-Headers"] = "charset, pwd, secret, name, mail, newpwd"
95
+ 200
96
+ end
97
+
98
+ #
99
+ # API Endpoints for User resources
100
+ #
101
+
102
+ #
103
+ # Lists all registered users.
104
+ #
105
+ # @return 200 OK, Response body includes JSON list of all registered users, list might be empty)
106
+ # Due to the fact that this request can be send unauthenticated, user data never!!! include data
107
+ # about games that are played by a user.
108
+ #
109
+ get "/users" do
110
+ JSON.pretty_generate(memory['users'])
111
+ end
112
+
113
+ #
114
+ # Creates a user.
115
+ #
116
+ # @param pwd Password for the user (used for authentication). Required. Parameter is part of request body.
117
+ # @param name Name of the user to provided new name. Required. Parameter is part of request body.
118
+ # @param mail Mail of the user. Optional. Parameter is part of request body.
119
+ #
120
+ # @return 200 OK, on successfull creation (response body includes JSON representation of updated user)
121
+ # @return 400, on invalid mail (response body includes error message)
122
+ # @return 409, on already existing new name (response body includes error message)
123
+ #
124
+ post "/user" do
125
+
126
+ name = params['name']
127
+ pwd = params['pwd']
128
+ mail = params['mail']
129
+ id = SecureRandom.uuid
130
+
131
+ unless mail =~ Defaults::VALID_EMAIL_REGEX || mail == nil
132
+ status 400
133
+ return "Bad Request: '#{mail}' is not a valid email."
134
+ end
135
+
136
+ if memory['users'].map { |entity| entity['name'] }.include? name
137
+ status 409
138
+ return "User with name '#{params['name']}' exists already."
139
+ end
140
+
141
+ user = {
142
+ "type" => 'user',
143
+ "name" => params['name'],
144
+ "id" => id,
145
+ "created" => "#{ Time.now.utc.iso8601(6) }",
146
+ "mail" => mail,
147
+ "signature" => Auth::signature(id, pwd)
148
+ }
149
+
150
+ memory['users'] << user
151
+ File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
152
+ JSON.pretty_generate(user)
153
+ end
154
+
155
+ #
156
+ # Retrieves user data.
157
+ #
158
+ # @param :id Unique identifier (or name, if called by byname option )of the user (the id is never changed!). Required. Parameter is part of the REST-URI!
159
+ # @param pwd Existing password of the user (used for authentication). Required. Parameter is part of request body.
160
+ # @param byname {=true, =false} Indicates that look up should be done by name (and not by identifier, which is the default). Optional. Parameter is part of request body.
161
+
162
+ # @return 200 OK, response body includes JSON representation of user
163
+ # @return 400, Bad Request, if byname parameter is set but not set to 'true' or 'false'
164
+ # @return 401, if request is not provided with correct password (response body includes error message)
165
+ # @return 404, if user with id is not present (response body includes error message)
166
+ #
167
+ get "/user/:id" do
168
+ pwd = params['pwd']
169
+ id = params['id']
170
+ byname = params['byname']
171
+
172
+ if !byname.nil? && byname != 'true' && byname != 'false'
173
+ status 400
174
+ return "Bad Request: byname parameter must be 'true' or 'false' (if set), was '#{byname}'."
175
+ end
176
+
177
+ user = service.get_user_by_id(id) if byname == 'false' || byname == nil
178
+ user = service.get_user_by_name(URI.decode(id)) if byname == 'true'
179
+
180
+ if user == nil
181
+ status 404
182
+ return "not found"
183
+ end
184
+
185
+ user = user.clone
186
+
187
+ unless Auth::authentic?(user, pwd)
188
+ status 401
189
+ return "unauthorized, please provide correct credentials"
190
+ end
191
+
192
+ user['games'] = memory['gamestates'].select { |state| state['userid'] == user['id'] }
193
+ .map { |state| state['gameid'] }
194
+ .uniq
195
+
196
+ JSON.pretty_generate(user)
197
+ end
198
+
199
+ #
200
+ # Updates a user.
201
+ #
202
+ # @param :id Unique identifier of the user (the id is never changed!). Required. Parameter is part of the REST-URI!
203
+ # @param pwd Existing password of the user (used for authentication). Required. Parameter is part of request body.
204
+ # @param new_name Changes name of the user to provided new name. Optional. Parameter is part of request body.
205
+ # @param new_mail Changes mail of the user to provided new mail. Optional. Parameter is part of request body.
206
+ # @param new_pwd Changes password of the user to a new password. Optional. Parameter is part of request body.
207
+ #
208
+ # @return 200 OK, on successfull update (response body includes JSON representation of updated user)
209
+ # @return 400, on invalid mail (response body includes error message)
210
+ # @return 401, on non matching access credentials (response body includes error message)
211
+ # @return 409, on already existing new name (response body includes error message)
212
+ #
213
+ put "/user/:id" do
214
+ id = params['id']
215
+ pwd = params['pwd']
216
+ new_name = params['name']
217
+ new_mail = params['mail']
218
+ new_pwd = params['newpwd']
219
+
220
+ if new_mail
221
+ unless new_mail =~ Defaults::VALID_EMAIL_REGEX
222
+ status 400
223
+ return "Bad Request: '#{new_mail}' is not a valid email."
224
+ end
225
+ end
226
+
227
+ if memory['users'].map { |entity| entity['name'] }.include? new_name
228
+ status 409
229
+ return "User with name '#{new_name}' exists already."
230
+ end
231
+
232
+ begin
233
+ user = service.get_user_by_id(id)
234
+
235
+ unless Auth::authentic?(user, pwd)
236
+ status 401
237
+ return "unauthorized, please provide correct credentials"
238
+ end
239
+
240
+ user['name'] = new_name if new_name != nil
241
+ user['mail'] = new_mail if new_mail != nil
242
+ user['signature'] = Auth::signature(id, new_pwd) if new_pwd != nil
243
+ user['update'] = "#{ Time.now.utc.iso8601(6) }"
244
+
245
+ File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
246
+ return JSON.pretty_generate(user)
247
+ rescue Exception => ex
248
+ status 401
249
+ return "#{ex}\nunauthorized, please provide correct credentials"
250
+ end
251
+ end
252
+
253
+ #
254
+ # Deletes a user and all of its associated game states.
255
+ #
256
+ # @param :id Unique identifier of the user (used for authentication). Required. Parameter is part of the REST-URI!
257
+ # @param pwd Existing password of the user (used for authentication). Required. Parameter is part of request body.
258
+ #
259
+ # @return 200 OK, on successfull delete (response body includes confirmation message)
260
+ # @return 401, on non matching access credentials (response body includes error message)
261
+ #
262
+ delete "/user/:id" do
263
+
264
+ id = params['id']
265
+ pwd = params['pwd']
266
+
267
+ user = service.get_user_by_id(id)
268
+
269
+ if user == nil
270
+ # This is an idempotent operation.
271
+ return "User '#{id}' deleted successfully."
272
+ end
273
+
274
+ unless Auth::authentic?(user, pwd)
275
+ status 401
276
+ return "unauthorized, please provide correct credentials"
277
+ end
278
+
279
+ memory['users'].delete_if { |user| user['id'] == id }
280
+ memory['gamestates'].delete_if { |state| state['userid'] == id }
281
+ File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
282
+
283
+ "User '#{id}' deleted successfully."
284
+ end
285
+
286
+ #
287
+ # API Endpoints for Game resources
288
+ #
289
+
290
+ #
291
+ # Lists all registered games.
292
+ #
293
+ # @return 200 OK, Response body includes JSON list of all registered games, list might be empty)
294
+ # Due to the fact that this request can be send unauthenticated, game data never!!! include data
295
+ # about users that are playing a game.
296
+ #
297
+ get "/games" do
298
+ games = memory['games']
299
+ JSON.pretty_generate(games)
300
+ end
301
+
302
+ #
303
+ # Creates a game.
304
+ #
305
+ # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
306
+ # @param name Name of the game. Required. Parameter is part of request body.
307
+ # @param url URL of the game. Optional. Parameter is part of request body.
308
+ #
309
+ # @return 200 OK, on successfull creation (response body includes JSON representation of game)
310
+ # @return 400, on invalid url (response body includes error message)
311
+ # @return 400, on invalid name (response body includes error message)
312
+ # @return 409, if a game with provided name already exists (response body includes error message)
313
+ #
314
+ post "/game" do
315
+ name = params['name']
316
+ secret = params['secret']
317
+ url = params['url']
318
+
319
+ uri = URI.parse(url) rescue nil
320
+
321
+ if (name == nil || name.empty?)
322
+ status 400
323
+ return "Bad Request: '#{name}' is not a valid name"
324
+ end
325
+
326
+ if (uri != nil && !url.empty?)
327
+ if !uri.absolute?
328
+ status 400
329
+ return "Bad Request: '#{url}' is not a valid absolute url"
330
+ end
331
+
332
+ if !url =~ Defaults::VALID_URL_REGEX
333
+ status 400
334
+ return "Bad Request: '#{url}' is not a valid absolute url"
335
+ end
336
+ end
337
+
338
+ if memory['games'].map { |entity| entity['name'] }.include? name
339
+ status 409
340
+ return "Game with name '#{params['name']}' exists already."
341
+ end
342
+
343
+ id = SecureRandom.uuid
344
+
345
+ game = {
346
+ "type" => 'game',
347
+ "name" => params['name'],
348
+ "id" => id,
349
+ "url" => "#{uri}",
350
+ "signature" => Auth::signature(id, secret),
351
+ "created" => "#{ Time.now.utc.iso8601(6) }"
352
+ }
353
+
354
+ memory['games'] << game
355
+ File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
356
+ JSON.pretty_generate(game)
357
+ end
358
+
359
+ #
360
+ # Retrieves game data.
361
+ #
362
+ # @param :id Unique identifier of the game (the id is never changed!). Required. Parameter is part of the REST-URI!
363
+ # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
364
+ #
365
+ # @return 200 OK, response body includes JSON representation of user
366
+ # @return 401, if request is not provided with correct secret (response body includes error message)
367
+ # @return 404, if game with id is not present (response body includes error message)
368
+ #
369
+ get "/game/:id" do
370
+
371
+ secret = params['secret']
372
+ id = params['id']
373
+ game = service.get_game_by_id(id)
374
+
375
+ if game == nil
376
+ status 404
377
+ return "not found"
378
+ end
379
+
380
+ game = game.clone
381
+
382
+ unless Auth::authentic?(game, secret)
383
+ status 401
384
+ return "unauthorized, please provide correct credentials"
385
+ end
386
+
387
+ game['users'] = memory['gamestates'].select { |state| state['gameid'] == id }
388
+ .map { |state| state['userid'] }
389
+ .uniq
390
+
391
+ JSON.pretty_generate(game)
392
+ end
393
+
394
+ #
395
+ # Updates a game.
396
+ #
397
+ # @param :id Unique identifier of the game (the id is never changed!). Required. Parameter is part of the REST-URI!
398
+ # @param pwd Existing secret of the game (used for authentication). Required. Parameter is part of request body.
399
+ # @param new_name Changes name of the game to provided new name. Optional. Parameter is part of request body.
400
+ # @param new_mail Changes url of the game to provided new url. Optional. Parameter is part of request body.
401
+ # @param new_secret Changes secret of the game to a new secret. Optional. Parameter is part of request body.
402
+ #
403
+ # @return 200 OK, on successfull update (response body includes JSON representation of updated game)
404
+ # @return 400, on invalid url (response body includes error message)
405
+ # @return 401, on non matching access credentials (response body includes error message)
406
+ # @return 409, on already existing new name (response body includes error message)
407
+ #
408
+ put "/game/:id" do
409
+ id = params['id']
410
+ secret = params['secret']
411
+ new_name = params['name']
412
+ new_url = params['url']
413
+ new_secret = params['newsecret']
414
+
415
+ uri = URI(new_url) rescue nil
416
+
417
+ if uri != nil && (!new_url =~ Defaults::VALID_URL_REGEX || !uri.absolute?)
418
+ status 400
419
+ return "Bad Request: '#{new_url}' is not a valid url."
420
+ end
421
+
422
+ if memory['games'].map { |entity| entity['name'] }.include? new_name
423
+ status 409
424
+ return "Game with name '#{new_name}' exists already."
425
+ end
426
+
427
+ begin
428
+ game = service.get_game_by_id(id)
429
+
430
+ unless Auth::authentic?(game, secret)
431
+ status 401
432
+ return "unauthorized, please provide correct credentials"
433
+ end
434
+
435
+ game['name'] = new_name if new_name != nil
436
+ game['url'] = new_url if new_url != nil
437
+ game['signature'] = Auth::signature(id, new_secret) if new_secret != nil
438
+ game['update'] = "#{ Time.now.utc.iso8601(6) }"
439
+
440
+ File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
441
+ return JSON.pretty_generate(game)
442
+ rescue Exception => ex
443
+ status 401
444
+ return "#{ex}\nunauthorized, please provide correct credentials"
445
+ end
446
+
447
+ end
448
+
449
+ #
450
+ # Deletes a game and all of its associated game states.
451
+ #
452
+ # @param :id Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
453
+ # @param secret Existing secret of the game (used for authentication). Required. Parameter is part of request body.
454
+ #
455
+ # @return 200 OK, on successfull delete (response body includes confirmation message)
456
+ # @return 401, on non matching access credentials (response body includes error message)
457
+ #
458
+ delete "/game/:id" do
459
+ id = params['id']
460
+ secret = params['secret']
461
+
462
+ game = service.get_game_by_id(id)
463
+
464
+ if game == nil
465
+ # This is an idempotent operation.
466
+ return "Game '#{id}' deleted successfully."
467
+ end
468
+
469
+ unless Auth::authentic?(game, secret)
470
+ status 401
471
+ return "unauthorized, please provide correct credentials"
472
+ end
473
+
474
+ memory['games'].delete_if { |game| game['id'] == id }
475
+ memory['gamestates'].delete_if { |state| state['gameid'] == id }
476
+ File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
477
+
478
+ "Game '#{id}' deleted successfully."
479
+ end
480
+
481
+ #
482
+ # API Endpoint for Gamestate resources
483
+ #
484
+
485
+ #
486
+ # Retrieves all gamestates stored for a game and a user.
487
+ # Gamestates are returned sorted by decreasing creation timestamps.
488
+ #
489
+ # @param :gameid Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
490
+ # @param :userid Unique identifier of the user (used for authentication). Required. Parameter is part of the REST-URI!
491
+ # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
492
+ #
493
+ # @return 200 OK, (response body includes confirmation message)
494
+ # @return 401, on non matching access credentials (response body includes error message)
495
+ # @return 404, not found (in case of gameid or userid are not existing) (response body includes error message)
496
+ #
497
+ get "/gamestate/:gameid/:userid" do
498
+
499
+ gameid = params['gameid']
500
+ userid = params['userid']
501
+ secret = params['secret']
502
+
503
+ game = service.get_game_by_id(gameid)
504
+ user = service.get_user_by_id(userid)
505
+
506
+ if game == nil || user == nil
507
+ status 404
508
+ "not found"
509
+ end
510
+
511
+ unless Auth::authentic?(game, secret)
512
+ status 401
513
+ return "unauthorized, please provide correct game credentials"
514
+ end
515
+
516
+ states = memory['gamestates'].select do |state|
517
+ state['gameid'] == gameid && state['userid'] == userid
518
+ end
519
+
520
+ return JSON.pretty_generate(states.map { |state|
521
+ r = state.clone
522
+ r['gamename'] = service.get_game_by_id(r['gameid'])['name']
523
+ r['username'] = service.get_user_by_id(r['userid'])['name']
524
+ r
525
+ }.sort { |b, a| Time.parse(a['created']) <=> Time.parse(b['created']) })
526
+ end
527
+
528
+ #
529
+ # Retrieves all gamestates stored for a game.
530
+ # Gamestates are returned sorted by decreasing creation timestamps.
531
+ #
532
+ # @param :gameid Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
533
+ # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
534
+ #
535
+ # @return 200 OK, (response body includes confirmation message)
536
+ # @return 401, on non matching access credentials (response body includes error message)
537
+ # @return 404, not found (in case of gameid or userid are not existing) (response body includes error message)
538
+ #
539
+ get "/gamestate/:gameid" do
540
+
541
+ gameid = params['gameid']
542
+ secret = params['secret']
543
+
544
+ game = service.get_game_by_id(gameid)
545
+
546
+ if game == nil
547
+ status 404
548
+ "not found"
549
+ end
550
+
551
+ unless Auth::authentic?(game, secret)
552
+ status 401
553
+ return "unauthorized, please provide correct game credentials"
554
+ end
555
+
556
+ states = memory['gamestates'].select do |state|
557
+ state['gameid'] == gameid
558
+ end
559
+
560
+ return JSON.pretty_generate(states.map { |state|
561
+ r = state.clone
562
+ r['gamename'] = service.get_game_by_id(r['gameid'])['name']
563
+ r['username'] = service.get_user_by_id(r['userid'])['name']
564
+ r
565
+ }.sort { |b, a| Time.parse(a['created']) <=> Time.parse(b['created']) })
566
+ end
567
+
568
+ #
569
+ # Stores a gamestate for a game and a user.
570
+ #
571
+ # @param :gameid Unique identifier of the game (used for authentication). Required. Parameter is part of the REST-URI!
572
+ # @param :userid Unique identifier of the user (used for authentication). Required. Parameter is part of the REST-URI!
573
+ # @param secret Secret of the game (used for authentication). Required. Parameter is part of request body.
574
+ # @param state JSON encoded gamestate to store. Required.
575
+ #
576
+ # @return 200 OK (response body includes confirmation message)
577
+ # @return 400, Bad request, in case of gamestate was empty or not encoded as valid JSON (response body includes error message)
578
+ # @return 401, on non matching access credentials of game (response body includes error message)
579
+ # @return 404, not found (in case of gameid or userid are not existing) (response body includes error message)
580
+ #
581
+ post "/gamestate/:gameid/:userid" do
582
+
583
+ gameid = params['gameid']
584
+ userid = params['userid']
585
+ secret = params['secret']
586
+ state = params['state']
587
+
588
+ game = service.get_game_by_id(gameid)
589
+ user = service.get_user_by_id(userid)
590
+
591
+ unless game != nil && user != nil
592
+ status 404
593
+ return "game id or user id not found"
594
+ end
595
+
596
+ unless Auth::authentic?(game, secret)
597
+ status 401
598
+ return "unauthorized, please provide correct game credentials"
599
+ end
600
+
601
+ begin
602
+ state = JSON.parse(state)
603
+
604
+ if state.empty?
605
+ status 400
606
+ return "Bad request: state must not be empty, was #{state}"
607
+ end
608
+
609
+ memory['gamestates'] << {
610
+ "type" => 'gamestate',
611
+ "gameid" => gameid,
612
+ "userid" => userid,
613
+ "created" => "#{ Time.now.utc.iso8601(6) }",
614
+ "state" => state
615
+ }
616
+
617
+ rescue
618
+ status 400
619
+ return "Bad request: state must be provided as valid JSON, was #{state}"
620
+ end
621
+
622
+ File.open(storage, "w") { |file| file.write(JSON.pretty_generate(memory)) }
623
+
624
+ "Added state."
625
+
626
+ end
627
+
628
+ end
629
+
630
+ end
631
+
632
+ end