lapis-yggdrasil 0.5.0

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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +18 -0
  3. data/.gitignore +151 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +4 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE.md +16 -0
  9. data/README.md +130 -0
  10. data/Rakefile +28 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +7 -0
  13. data/lapis-yggdrasil.gemspec +35 -0
  14. data/lib/lapis/yggdrasil.rb +18 -0
  15. data/lib/lapis/yggdrasil/agent.rb +52 -0
  16. data/lib/lapis/yggdrasil/authentication_client.rb +106 -0
  17. data/lib/lapis/yggdrasil/authentication_error.rb +29 -0
  18. data/lib/lapis/yggdrasil/client.rb +38 -0
  19. data/lib/lapis/yggdrasil/error_codes.rb +43 -0
  20. data/lib/lapis/yggdrasil/messaging.rb +23 -0
  21. data/lib/lapis/yggdrasil/messaging/authentication_request.rb +50 -0
  22. data/lib/lapis/yggdrasil/messaging/authentication_response.rb +36 -0
  23. data/lib/lapis/yggdrasil/messaging/error_response.rb +74 -0
  24. data/lib/lapis/yggdrasil/messaging/invalidate_request.rb +34 -0
  25. data/lib/lapis/yggdrasil/messaging/refresh_request.rb +47 -0
  26. data/lib/lapis/yggdrasil/messaging/refresh_response.rb +29 -0
  27. data/lib/lapis/yggdrasil/messaging/request.rb +25 -0
  28. data/lib/lapis/yggdrasil/messaging/response.rb +33 -0
  29. data/lib/lapis/yggdrasil/messaging/response_factory.rb +82 -0
  30. data/lib/lapis/yggdrasil/messaging/signout_request.rb +38 -0
  31. data/lib/lapis/yggdrasil/messaging/token_request.rb +42 -0
  32. data/lib/lapis/yggdrasil/messaging/token_response.rb +36 -0
  33. data/lib/lapis/yggdrasil/messaging/validate_request.rb +34 -0
  34. data/lib/lapis/yggdrasil/profile.rb +65 -0
  35. data/lib/lapis/yggdrasil/session.rb +60 -0
  36. data/lib/lapis/yggdrasil/session_info.rb +62 -0
  37. data/lib/lapis/yggdrasil/version.rb +5 -0
  38. data/spec/factories/agent_factory.rb +10 -0
  39. data/spec/factories/authentication_error_factory.rb +14 -0
  40. data/spec/factories/message_factory.rb +111 -0
  41. data/spec/factories/profile_factory.rb +20 -0
  42. data/spec/factories/session_info_factory.rb +11 -0
  43. data/spec/factories/uuid_factory.rb +28 -0
  44. data/spec/lapis/yggdrasil/agent_spec.rb +103 -0
  45. data/spec/lapis/yggdrasil/authentication_client_spec.rb +200 -0
  46. data/spec/lapis/yggdrasil/authentication_error_spec.rb +42 -0
  47. data/spec/lapis/yggdrasil/messaging/authentication_request_spec.rb +61 -0
  48. data/spec/lapis/yggdrasil/messaging/authentication_response_spec.rb +63 -0
  49. data/spec/lapis/yggdrasil/messaging/error_response_spec.rb +164 -0
  50. data/spec/lapis/yggdrasil/messaging/invalidate_request_spec.rb +29 -0
  51. data/spec/lapis/yggdrasil/messaging/refresh_request_spec.rb +70 -0
  52. data/spec/lapis/yggdrasil/messaging/refresh_response_spec.rb +50 -0
  53. data/spec/lapis/yggdrasil/messaging/response_factory_spec.rb +130 -0
  54. data/spec/lapis/yggdrasil/messaging/response_spec.rb +36 -0
  55. data/spec/lapis/yggdrasil/messaging/signout_request_spec.rb +29 -0
  56. data/spec/lapis/yggdrasil/messaging/validate_request_spec.rb +29 -0
  57. data/spec/lapis/yggdrasil/profile_spec.rb +108 -0
  58. data/spec/lapis/yggdrasil/session_info_spec.rb +131 -0
  59. data/spec/lapis/yggdrasil/session_spec.rb +158 -0
  60. data/spec/spec_helper.rb +89 -0
  61. metadata +269 -0
@@ -0,0 +1,38 @@
1
+ require 'httpclient'
2
+
3
+ module Lapis
4
+ module Yggdrasil
5
+
6
+ # Provides low-level interaction with an REST-ful server.
7
+ # @abstract
8
+ class Client
9
+
10
+ private
11
+
12
+ # HTTP headers to send as part of requests.
13
+ HEADERS = {
14
+ 'Content-Type' => 'application/json'
15
+ }.freeze
16
+
17
+ protected
18
+
19
+ # Creates a new interface to an authentication server.
20
+ # @param url [String] Base URL of the authentication server.
21
+ def initialize(url)
22
+ @url = url.dup.freeze
23
+ @client = HTTPClient.new
24
+ end
25
+
26
+ # Sends a request to the server and waits for a response.
27
+ # @param message [Messaging::Request] Request to send.
28
+ # @return [HTTP::Message] Response received from the server.
29
+ def send_request(message)
30
+ url = @url + message.endpoint
31
+ body = message.to_json
32
+ @client.post(url, :body => body, :header => HEADERS)
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,43 @@
1
+ module Lapis
2
+ module Yggdrasil
3
+
4
+ # Constants describing a type of error.
5
+ # @see Error
6
+ module ErrorCodes
7
+
8
+ # The method specified in the request is not allowed for the resource identified by the request URI.
9
+ # Something other than a `POST` request was received.
10
+ METHOD_NOT_ALLOWED = :method_not_allowed
11
+
12
+ # The server has not found anything matches the request URI.
13
+ # Non-existing endpoint was called.
14
+ NOT_FOUND = :not_found
15
+
16
+ # Invalid credentials - account has migrated to Mojang account.
17
+ # Use e-mail as username.
18
+ ACCOUNT_MIGRATED = :account_migrated
19
+
20
+ # Invalid credentials - bad username or password.
21
+ INVALID_CREDENTIALS = :invalid_credentials
22
+
23
+ # Invalid credentials - too many login attempts with this username recently.
24
+ # @note Username and password may still be valid.
25
+ TOO_MANY_ATTEMPTS = :too_many_attempts
26
+
27
+ # Invalid token - provided access token is invalid.
28
+ INVALID_TOKEN = :invalid_token
29
+
30
+ # Access token already has a profile assigned.
31
+ # Selecting profiles isn't implemented yet.
32
+ PROFILE_NOT_SUPPORTED = :profile_not_supported
33
+
34
+ # Username and password was not provided.
35
+ NO_CREDENTIALS = :no_credentials
36
+
37
+ # The server received something other than a message body with JSON.
38
+ UNSUPPORTED_MEDIA_TYPE = :unsupported_media_type
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module Lapis
2
+ module Yggdrasil
3
+
4
+ # Representation of requests and responses sent to and from the authentication server.
5
+ module Messaging
6
+ end
7
+
8
+ end
9
+ end
10
+
11
+ require_relative 'messaging/request'
12
+ require_relative 'messaging/token_request'
13
+ require_relative 'messaging/authentication_request'
14
+ require_relative 'messaging/refresh_request'
15
+ require_relative 'messaging/validate_request'
16
+ require_relative 'messaging/signout_request'
17
+ require_relative 'messaging/invalidate_request'
18
+ require_relative 'messaging/response'
19
+ require_relative 'messaging/error_response'
20
+ require_relative 'messaging/token_response'
21
+ require_relative 'messaging/authentication_response'
22
+ require_relative 'messaging/refresh_response'
23
+ require_relative 'messaging/response_factory'
@@ -0,0 +1,50 @@
1
+ require 'json'
2
+ require_relative 'request'
3
+
4
+ module Lapis
5
+ module Yggdrasil
6
+ module Messaging
7
+
8
+ # Message sent to attempt to authenticate.
9
+ # @see AuthenticationResponse
10
+ class AuthenticationRequest < Request
11
+
12
+ # Creates a new authentication request.
13
+ # @param username [String] Minecraft username or Mojang account email address.
14
+ # @param password [String] Password for the user.
15
+ # @param agent [Agent] Game information being requested.
16
+ # @param client_token [Uuid, nil] ID for the client instance.
17
+ # Provide +nil+ to have the authentication server provide one.
18
+ def initialize(username, password, agent, client_token = nil)
19
+ @username = username.dup.freeze
20
+ @password = password.dup.freeze
21
+ @agent = agent
22
+ @client_token = client_token
23
+ end
24
+
25
+ # Path on the server that handles the request.
26
+ # @return [String] Path to handler.
27
+ def endpoint
28
+ '/authenticate'
29
+ end
30
+
31
+ # Generates a JSON string that can be sent to an authentication server.
32
+ # @return [String] JSON string.
33
+ def to_json
34
+ hash = {
35
+ :agent => {
36
+ :name => @agent.name,
37
+ :version => @agent.version
38
+ },
39
+ :username => @username,
40
+ :password => @password
41
+ }
42
+ hash[:clientToken] = @client_token.to_s(false) if @client_token
43
+ hash.to_json
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ require 'json'
2
+ require_relative 'token_response'
3
+ require_relative '../profile'
4
+
5
+ module Lapis
6
+ module Yggdrasil
7
+ module Messaging
8
+
9
+ # Response to a an authentication request.
10
+ # @see AuthenticationRequest
11
+ class AuthenticationResponse < TokenResponse
12
+
13
+ # List of profiles to choose from.
14
+ # @return [Array<Profile>]
15
+ attr_reader :available_profiles
16
+
17
+ # Currently active profile for the user's selected game.
18
+ # @return [Profile]
19
+ attr_reader :selected_profile
20
+
21
+ # Creates a new authentication response by parsing a message from the server.
22
+ # @param message [HTTP::Message] Authentication message from the server.
23
+ def initialize(message)
24
+ super(message)
25
+ hash = JSON.parse(message.body, :symbolize_names => true)
26
+ @available_profiles = hash[:availableProfiles].map do |properties|
27
+ Profile.from_properties(properties)
28
+ end.freeze
29
+ @selected_profile = Profile.from_properties(hash[:selectedProfile])
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,74 @@
1
+ require 'json'
2
+ require_relative 'response'
3
+ require_relative '../authentication_error'
4
+ require_relative '../error_codes'
5
+
6
+ module Lapis
7
+ module Yggdrasil
8
+ module Messaging
9
+
10
+ # Response to a request indicating a problem occurred.
11
+ class ErrorResponse < Response
12
+
13
+ # Keywords describing the problem.
14
+ # @return [String]
15
+ attr_reader :error
16
+
17
+ # Brief description of the problem.
18
+ # @return [String]
19
+ attr_reader :message
20
+
21
+ # Additional information about the root of the problem.
22
+ # @return [String] Additional information.
23
+ # @return [nil] No additional information available.
24
+ attr_reader :cause
25
+
26
+ # Creates a new error response by parsing a message from the server.
27
+ # @param message [HTTP::Message] Error message from the server.
28
+ def initialize(message)
29
+ super(message)
30
+ hash = JSON.parse(message.body)
31
+ @error = hash['error']
32
+ @message = hash['errorMessage']
33
+ @cause = hash['cause']
34
+ end
35
+
36
+ # Creates a throwable exception from the response information.
37
+ # @return [AuthenticationError]
38
+ def to_error
39
+ code = case @error
40
+ when 'Method Not Allowed'
41
+ ErrorCodes::METHOD_NOT_ALLOWED
42
+ when 'Not Found'
43
+ ErrorCodes::NOT_FOUND
44
+ when 'ForbiddenOperationException'
45
+ if @cause == 'UserMigratedException'
46
+ ErrorCodes::ACCOUNT_MIGRATED
47
+ else
48
+ case @message
49
+ when /username/
50
+ ErrorCodes::INVALID_CREDENTIALS
51
+ when /credentials/
52
+ ErrorCodes::TOO_MANY_ATTEMPTS
53
+ when /token/
54
+ ErrorCodes::INVALID_TOKEN
55
+ end
56
+ end
57
+ when 'IllegalArgumentException'
58
+ case @message
59
+ when /profile/
60
+ ErrorCodes::PROFILE_NOT_SUPPORTED
61
+ when /credentials/
62
+ ErrorCodes::NO_CREDENTIALS
63
+ end
64
+ when 'Unsupported Media Type'
65
+ ErrorCodes::UNSUPPORTED_MEDIA_TYPE
66
+ end
67
+ AuthenticationError.new(code, @message)
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+ require_relative 'token_request'
3
+
4
+ module Lapis
5
+ module Yggdrasil
6
+ module Messaging
7
+
8
+ # Request for ending a session.
9
+ class InvalidateRequest < TokenRequest
10
+
11
+ # Creates a new invalidate request.
12
+ # @param client_token [Uuid] Unique ID Of the client that performed the authentication.
13
+ # @param access_token [Uuid] Unique ID of the authentication session.
14
+ def initialize(client_token, access_token)
15
+ super(client_token, access_token)
16
+ end
17
+
18
+ # Path on the server that handles the request.
19
+ # @return [String] Path to handler.
20
+ def endpoint
21
+ '/invalidate'
22
+ end
23
+
24
+ # Generates a JSON string that can be sent to an authentication server.
25
+ # @return [String] JSON string.
26
+ def to_json
27
+ structure.to_json
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ require 'json'
2
+ require_relative 'token_request'
3
+
4
+ module Lapis
5
+ module Yggdrasil
6
+ module Messaging
7
+
8
+ # Request for renewing a previously used authentication session.
9
+ # @see RefreshResponse
10
+ class RefreshRequest < TokenRequest
11
+
12
+ # Creates a new refresh request.
13
+ # @param client_token [Uuid] Unique ID Of the client that performed the authentication.
14
+ # @param access_token [Uuid] Unique ID of the authentication session.
15
+ # @param profile [Profile, nil] New profile to switch to.
16
+ # Specify +nil+ to keep the same profile.
17
+ def initialize(client_token, access_token, profile = nil)
18
+ super(client_token, access_token)
19
+ @profile = profile
20
+ end
21
+
22
+ # Path on the server that handles the request.
23
+ # @return [String] Path to handler.
24
+ def endpoint
25
+ '/refresh'
26
+ end
27
+
28
+ # Generates a JSON string that can be sent to an authentication server.
29
+ # @return [String] JSON string.
30
+ def to_json
31
+ hash = structure
32
+ if @profile
33
+ profile_hash = {
34
+ :name => @profile.name,
35
+ :id => @profile.id.to_s(false)
36
+ }
37
+ profile_hash[:legacy] = true if @profile.legacy?
38
+ hash[:selectedProfile] = profile_hash
39
+ end
40
+ hash.to_json
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+ require_relative 'token_response'
3
+ require_relative '../profile'
4
+
5
+ module Lapis
6
+ module Yggdrasil
7
+ module Messaging
8
+
9
+ # Response to a session renewal (refresh) request.
10
+ # @see RefreshRequest
11
+ class RefreshResponse < TokenResponse
12
+
13
+ # Profile selected for the user.
14
+ # @return [Profile]
15
+ attr_reader :profile
16
+
17
+ # Creates a new refresh response by parsing a message received from the server.
18
+ # @param message [HTTP::Message] Refresh response from the server.
19
+ def initialize(message)
20
+ super(message)
21
+ hash = JSON.parse(message.body, :symbolize_names => true)
22
+ @profile = Profile.from_properties(hash[:selectedProfile])
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Lapis
2
+ module Yggdrasil
3
+ module Messaging
4
+
5
+ # Base class for all request messages.
6
+ # @abstract
7
+ class Request
8
+
9
+ # Path on the server that handles the request.
10
+ # @return [String] Path to handler.
11
+ def endpoint
12
+ fail NotImplementedError, 'Sub-class must implement this method'
13
+ end
14
+
15
+ # Generates a JSON string that can be sent to an authentication server.
16
+ # @return [String] JSON string.
17
+ def to_json
18
+ fail NotImplementedError, 'Sub-class must implement this method'
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,33 @@
1
+ module Lapis
2
+ module Yggdrasil
3
+ module Messaging
4
+
5
+ # Base class for all response messages received from the server.
6
+ # @abstract
7
+ class Response
8
+
9
+ # HTTP status code given in the response.
10
+ # @return [Fixnum] HTTP status code.
11
+ attr_reader :status
12
+
13
+ # Determines whether the result of the request was a success.
14
+ # @return [true] The result was a success.
15
+ # @return [false] The result was a failure.
16
+ def ok?
17
+ @is_ok
18
+ end
19
+
20
+ protected
21
+
22
+ # Creates the base of the response by parsing a response message.
23
+ # @param message [HTTP::Message] Message received from the server.
24
+ def initialize(message)
25
+ @status = message.status
26
+ @is_ok = message.ok?
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
33
+ end