ruby_home 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +13 -14
  3. data/README.md +1 -1
  4. data/bin/rubyhome +1 -1
  5. data/lib/ruby_home/accessory_info.rb +33 -72
  6. data/lib/ruby_home/device_id.rb +0 -2
  7. data/lib/ruby_home/dns/service.rb +1 -6
  8. data/lib/ruby_home/dns/text_record.rb +0 -2
  9. data/lib/ruby_home/factories/accessory_factory.rb +0 -5
  10. data/lib/ruby_home/factories/characteristic_factory.rb +0 -3
  11. data/lib/ruby_home/factories/templates/characteristic_template.rb +0 -1
  12. data/lib/ruby_home/factories/templates/service_template.rb +0 -2
  13. data/lib/ruby_home/hap/accessory.rb +2 -2
  14. data/lib/ruby_home/hap/characteristic.rb +2 -2
  15. data/lib/ruby_home/hap/crypto/chacha20poly1305.rb +0 -4
  16. data/lib/ruby_home/hap/crypto/hkdf.rb +0 -2
  17. data/lib/ruby_home/hap/http_decryption.rb +5 -6
  18. data/lib/ruby_home/hap/http_encryption.rb +21 -11
  19. data/lib/ruby_home/hap/service.rb +0 -3
  20. data/lib/ruby_home/hap/tlv.rb +28 -21
  21. data/lib/ruby_home/hex_helper.rb +15 -0
  22. data/lib/ruby_home/http/application.rb +24 -19
  23. data/lib/ruby_home/http/controllers/accessories_controller.rb +1 -2
  24. data/lib/ruby_home/http/controllers/application_controller.rb +60 -13
  25. data/lib/ruby_home/http/controllers/characteristics_controller.rb +2 -3
  26. data/lib/ruby_home/http/controllers/pair_setups_controller.rb +41 -78
  27. data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +22 -24
  28. data/lib/ruby_home/http/controllers/pairings_controller.rb +8 -8
  29. data/lib/ruby_home/http/hap_request.rb +2 -6
  30. data/lib/ruby_home/http/hap_response.rb +2 -8
  31. data/lib/ruby_home/http/hap_server.rb +7 -12
  32. data/lib/ruby_home/http/serializers/object_serializer.rb +0 -2
  33. data/lib/ruby_home/http/services/start_srp_service.rb +46 -0
  34. data/lib/ruby_home/http/services/verify_srp_service.rb +55 -0
  35. data/lib/ruby_home/rack/handler/hap_server.rb +2 -7
  36. data/lib/ruby_home/version.rb +1 -1
  37. data/lib/ruby_home/yaml_record.rb +440 -0
  38. data/lib/ruby_home.rb +45 -6
  39. data/rubyhome.gemspec +4 -4
  40. metadata +44 -49
  41. data/lib/ruby_home/broadcast.rb +0 -31
  42. data/lib/ruby_home/hap/hex_pad.rb +0 -13
  43. data/lib/ruby_home/http/cache.rb +0 -30
@@ -1,10 +1,9 @@
1
1
  require_relative 'application_controller'
2
- require_relative '../serializers/characteristic_value_serializer'
3
2
 
4
3
  module RubyHome
5
4
  module HTTP
6
5
  class CharacteristicsController < ApplicationController
7
- get '/characteristics' do
6
+ get '/' do
8
7
  content_type 'application/hap+json'
9
8
 
10
9
  if cache[:controller_to_accessory_key] && cache[:accessory_to_controller_key]
@@ -21,7 +20,7 @@ module RubyHome
21
20
  end
22
21
  end
23
22
 
24
- put '/characteristics' do
23
+ put '/' do
25
24
  content_type 'application/hap+json'
26
25
 
27
26
  if cache[:controller_to_accessory_key] && cache[:accessory_to_controller_key]
@@ -1,81 +1,75 @@
1
- require 'hkdf'
2
- require 'openssl'
3
- require 'rbnacl/libsodium'
4
- require 'ruby_home-srp'
5
- require_relative '../../hap/hex_pad'
6
- require_relative '../../hap/tlv'
7
1
  require_relative 'application_controller'
8
2
 
9
3
  module RubyHome
10
4
  module HTTP
11
5
  class PairSetupsController < ApplicationController
12
- post '/pair-setup' do
6
+ post '/' do
13
7
  content_type 'application/pairing+tlv8'
14
8
 
15
- case unpack_request['kTLVType_State']
9
+ case unpack_request[:state]
16
10
  when 1
17
- srp_start_response
11
+ start
18
12
  when 3
19
- srp_verify_response
13
+ verify
20
14
  when 5
21
- exchange_response
15
+ exchange
22
16
  end
23
17
  end
24
18
 
25
19
  private
26
20
 
27
- def srp_start_response
28
- username = 'Pair-Setup'
29
- password = '031-45-154'
21
+ def pairing_failed
22
+ clear_cache
30
23
 
31
- auth = srp_verifier.generate_userauth(username, password)
24
+ tlv state: 4, error: 2
25
+ end
32
26
 
33
- verifier = auth[:verifier]
34
- salt = auth[:salt]
27
+ def start
28
+ start_srp = StartSRPService.new(
29
+ username: accessory_info.username,
30
+ password: accessory_info.password
31
+ )
35
32
 
36
- challenge_and_proof = srp_verifier.get_challenge_and_proof(username, verifier, salt)
37
- store_proof(challenge_and_proof[:proof])
33
+ cache[:srp_session] = start_srp.proof
38
34
 
39
- HAP::TLV.encode({
40
- 'kTLVType_Salt' => [challenge_and_proof[:challenge][:salt]].pack('H*'),
41
- 'kTLVType_PublicKey' => [challenge_and_proof[:challenge][:B]].pack('H*'),
42
- 'kTLVType_State' => 2
43
- })
35
+ tlv salt: start_srp.salt_bytes, public_key: start_srp.public_key_bytes, state: 2
44
36
  end
45
37
 
46
- def srp_verify_response
47
- proof = retrieve_proof.dup
48
- proof[:A] = unpack_request['kTLVType_PublicKey'].unpack1('H*')
49
-
50
- server_m2_proof = srp_verifier.verify_session(proof, unpack_request['kTLVType_Proof'].unpack1('H*'))
38
+ def verify
39
+ verify_srp = VerifySRPService.new(
40
+ device_proof: unpack_request[:proof],
41
+ srp_session: cache[:srp_session],
42
+ public_key: unpack_request[:public_key],
43
+ )
51
44
 
52
- store_session_key(srp_verifier.K)
53
- forget_proof!
45
+ if verify_srp.valid?
46
+ cache[:session_key] = verify_srp.session_key
47
+ cache.delete(:srp_session)
54
48
 
55
- HAP::TLV.encode({
56
- 'kTLVType_State' => 4,
57
- 'kTLVType_Proof' => [server_m2_proof].pack('H*')
58
- })
49
+ tlv state: 4, proof: verify_srp.server_proof
50
+ else
51
+ pairing_failed
52
+ end
59
53
  end
60
54
 
61
- def exchange_response
62
- encrypted_data = unpack_request['kTLVType_EncryptedData']
55
+ def exchange
56
+ encrypted_data = unpack_request[:encrypted_data]
63
57
 
64
58
  hkdf = HAP::Crypto::HKDF.new(info: 'Pair-Setup-Encrypt-Info', salt: 'Pair-Setup-Encrypt-Salt')
65
- key = hkdf.encrypt(session_key)
59
+ key = hkdf.encrypt(cache[:session_key])
66
60
 
67
61
  chacha20poly1305ietf = HAP::Crypto::ChaCha20Poly1305.new(key)
68
62
 
69
- nonce = HAP::HexPad.pad('PS-Msg05')
63
+ nonce = HexHelper.pad('PS-Msg05')
70
64
  decrypted_data = chacha20poly1305ietf.decrypt(nonce, encrypted_data)
71
65
  unpacked_decrypted_data = HAP::TLV.read(decrypted_data)
72
66
 
73
- iosdevicepairingid = unpacked_decrypted_data['kTLVType_Identifier']
74
- iosdevicesignature = unpacked_decrypted_data['kTLVType_Signature']
75
- iosdeviceltpk = unpacked_decrypted_data['kTLVType_PublicKey']
67
+ iosdevicepairingid = unpacked_decrypted_data[:identifier]
68
+ iosdevicesignature = unpacked_decrypted_data[:signature]
69
+ iosdeviceltpk = unpacked_decrypted_data[:public_key]
76
70
 
77
71
  hkdf = HAP::Crypto::HKDF.new(info: 'Pair-Setup-Controller-Sign-Info', salt: 'Pair-Setup-Controller-Sign-Salt')
78
- iosdevicex = hkdf.encrypt(session_key)
72
+ iosdevicex = hkdf.encrypt(cache[:session_key])
79
73
 
80
74
  iosdeviceinfo = [
81
75
  iosdevicex.unpack1('H*'),
@@ -86,7 +80,7 @@ module RubyHome
86
80
 
87
81
  if verify_key.verify(iosdevicesignature, [iosdeviceinfo].pack('H*'))
88
82
  hkdf = HAP::Crypto::HKDF.new(info: 'Pair-Setup-Accessory-Sign-Info', salt: 'Pair-Setup-Accessory-Sign-Salt')
89
- accessory_x = hkdf.encrypt(session_key)
83
+ accessory_x = hkdf.encrypt(cache[:session_key])
90
84
 
91
85
  signing_key = accessory_info.signing_key
92
86
  accessoryltpk = signing_key.verify_key.to_bytes
@@ -98,48 +92,17 @@ module RubyHome
98
92
 
99
93
  accessorysignature = signing_key.sign([accessoryinfo].pack('H*'))
100
94
 
101
- subtlv = HAP::TLV.encode({
102
- 'kTLVType_Identifier' => accessory_info.device_id,
103
- 'kTLVType_PublicKey' => accessoryltpk,
104
- 'kTLVType_Signature' => accessorysignature
105
- })
95
+ subtlv = tlv(identifier: accessory_info.device_id, public_key: accessoryltpk, signature: accessorysignature)
106
96
 
107
- nonce = HAP::HexPad.pad('PS-Msg06')
97
+ nonce = HexHelper.pad('PS-Msg06')
108
98
  encrypted_data = chacha20poly1305ietf.encrypt(nonce, subtlv)
109
99
 
110
100
  pairing_params = { admin: true, identifier: iosdevicepairingid, public_key: iosdeviceltpk.unpack1('H*') }
111
101
  accessory_info.add_paired_client pairing_params
112
102
 
113
- HAP::TLV.encode({
114
- 'kTLVType_State' => 6,
115
- 'kTLVType_EncryptedData' => encrypted_data
116
- })
103
+ tlv state: 6, encrypted_data: encrypted_data
117
104
  end
118
105
  end
119
-
120
- def srp_verifier
121
- @_verifier ||= RubyHome::SRP::Verifier.new
122
- end
123
-
124
- def store_proof(proof)
125
- cache[:proof] = proof
126
- end
127
-
128
- def retrieve_proof
129
- cache[:proof]
130
- end
131
-
132
- def forget_proof!
133
- cache[:proof] = nil
134
- end
135
-
136
- def store_session_key(key)
137
- cache[:session_key] = key
138
- end
139
-
140
- def session_key
141
- cache[:session_key]
142
- end
143
106
  end
144
107
  end
145
108
  end
@@ -1,16 +1,14 @@
1
- require 'x25519'
2
- require_relative '../../hap/crypto/hkdf'
3
- require_relative '../../hap/hex_pad'
4
- require_relative '../../hap/tlv'
5
1
  require_relative 'application_controller'
6
2
 
7
3
  module RubyHome
8
4
  module HTTP
9
5
  class PairVerifiesController < ApplicationController
10
- post '/pair-verify' do
6
+ post '/' do
11
7
  content_type 'application/pairing+tlv8'
12
8
 
13
- case unpack_request['kTLVType_State']
9
+ verify_accessory_paired
10
+
11
+ case unpack_request[:state]
14
12
  when 1
15
13
  verify_start_response
16
14
  when 3
@@ -21,10 +19,10 @@ module RubyHome
21
19
  private
22
20
 
23
21
  def verify_start_response
24
- secret_key = X25519::Scalar.generate
22
+ secret_key = RbNaCl::PrivateKey.generate
25
23
  public_key = secret_key.public_key.to_bytes
26
- client_public_key = X25519::MontgomeryU.new(unpack_request['kTLVType_PublicKey'])
27
- shared_secret = secret_key.multiply(client_public_key).to_bytes
24
+ client_public_key = RbNaCl::PublicKey.new(unpack_request[:public_key])
25
+ shared_secret = RbNaCl::GroupElement.new(client_public_key).mult(secret_key).to_bytes
28
26
  cache[:shared_secret] = shared_secret
29
27
 
30
28
  accessoryinfo = [
@@ -36,46 +34,46 @@ module RubyHome
36
34
  signing_key = accessory_info.signing_key
37
35
  accessorysignature = signing_key.sign([accessoryinfo].pack('H*'))
38
36
 
39
- subtlv = HAP::TLV.encode({
40
- 'kTLVType_Identifier' => accessory_info.device_id,
41
- 'kTLVType_Signature' => accessorysignature
42
- })
37
+ subtlv = tlv(identifier: accessory_info.device_id, signature: accessorysignature)
43
38
 
44
39
  hkdf = HAP::Crypto::HKDF.new(info: 'Pair-Verify-Encrypt-Info', salt: 'Pair-Verify-Encrypt-Salt')
45
40
  session_key = hkdf.encrypt(shared_secret)
46
41
  cache[:session_key] = session_key
47
42
 
48
43
  chacha20poly1305ietf = HAP::Crypto::ChaCha20Poly1305.new(session_key)
49
- nonce = HAP::HexPad.pad('PV-Msg02')
44
+ nonce = HexHelper.pad('PV-Msg02')
50
45
  encrypted_data = chacha20poly1305ietf.encrypt(nonce, subtlv)
51
46
 
52
- HAP::TLV.encode({
53
- 'kTLVType_State' => 2,
54
- 'kTLVType_PublicKey' => public_key,
55
- 'kTLVType_EncryptedData' => encrypted_data
56
- })
47
+ tlv state: 2, public_key: public_key, encrypted_data: encrypted_data
57
48
  end
58
49
 
59
50
  def verify_finish_response
60
- encrypted_data = unpack_request['kTLVType_EncryptedData']
51
+ encrypted_data = unpack_request[:encrypted_data]
61
52
 
62
53
  chacha20poly1305ietf = HAP::Crypto::ChaCha20Poly1305.new(cache[:session_key])
63
- nonce = HAP::HexPad.pad('PV-Msg03')
54
+ nonce = HexHelper.pad('PV-Msg03')
64
55
  decrypted_data = chacha20poly1305ietf.decrypt(nonce, encrypted_data)
65
56
  unpacked_decrypted_data = HAP::TLV.read(decrypted_data)
66
57
 
67
- if accessory_info.paired_clients.any? {|h| h[:identifier] == unpacked_decrypted_data['kTLVType_Identifier']}
58
+ if accessory_info.paired_clients.any? {|h| h[:identifier] == unpacked_decrypted_data[:identifier]}
68
59
  hkdf = HAP::Crypto::HKDF.new(info: 'Control-Write-Encryption-Key', salt: 'Control-Salt')
69
60
  cache[:controller_to_accessory_key] = hkdf.encrypt(cache[:shared_secret])
70
61
 
71
62
  hkdf = HAP::Crypto::HKDF.new(info: 'Control-Read-Encryption-Key', salt: 'Control-Salt')
72
63
  cache[:accessory_to_controller_key] = hkdf.encrypt(cache[:shared_secret])
73
64
 
74
- HAP::TLV.encode({'kTLVType_State' => 4})
65
+ cache.delete(:session_key)
66
+ cache.delete(:shared_secret)
67
+
68
+ tlv state: 4
75
69
  else
76
- HAP::TLV.encode({'kTLVType_State' => 4, 'kTLVType_Error' => 2})
70
+ tlv state: 4, error: 2
77
71
  end
78
72
  end
73
+
74
+ def verify_accessory_paired
75
+ halt 403 unless accessory_info.paired?
76
+ end
79
77
  end
80
78
  end
81
79
  end
@@ -3,10 +3,10 @@ require_relative 'application_controller'
3
3
  module RubyHome
4
4
  module HTTP
5
5
  class PairingsController < ApplicationController
6
- post '/pairings' do
6
+ post '/' do
7
7
  content_type 'application/pairing+tlv8'
8
8
 
9
- case unpack_request['kTLVType_Method']
9
+ case unpack_request[:method]
10
10
  when 3
11
11
  add_pairing
12
12
  when 4
@@ -18,20 +18,20 @@ module RubyHome
18
18
 
19
19
  def add_pairing
20
20
  pairing_params = {
21
- admin: !!unpack_request['kTLVType_Permissions'],
22
- identifier: unpack_request['kTLVType_Identifier'],
23
- public_key: unpack_request['kTLVType_PublicKey'].unpack1('H*')
21
+ admin: !!unpack_request[:permissions],
22
+ identifier: unpack_request[:identifier],
23
+ public_key: unpack_request[:public_key].unpack1('H*')
24
24
  }
25
25
  accessory_info.add_paired_client pairing_params
26
26
 
27
- HAP::TLV.encode({'kTLVType_State' => 2})
27
+ tlv state: 2
28
28
  end
29
29
 
30
30
  def remove_pairing
31
- accessory_info.remove_paired_client(unpack_request['kTLVType_Identifier'])
31
+ accessory_info.remove_paired_client(unpack_request[:identifier])
32
32
 
33
33
  response['connection'] = 'close'
34
- HAP::TLV.encode({'kTLVType_State' => 2})
34
+ tlv state: 2
35
35
  end
36
36
  end
37
37
  end
@@ -1,11 +1,7 @@
1
- require 'webrick/httprequest'
2
- require_relative '../hap/http_decryption'
3
-
4
1
  module RubyHome
5
2
  module HTTP
6
3
  class HAPRequest < WEBrick::HTTPRequest
7
- def initialize(*args, request_id: )
8
- @_request_id = request_id
4
+ def initialize(*args)
9
5
  cache[:controller_to_accessory_count] ||= 0
10
6
 
11
7
  super(*args)
@@ -49,7 +45,7 @@ module RubyHome
49
45
  end
50
46
 
51
47
  def cache
52
- GlobalCache.instance[@_request_id] ||= Cache.new
48
+ RequestStore.store
53
49
  end
54
50
  end
55
51
  end
@@ -1,11 +1,7 @@
1
- require 'webrick/httpresponse'
2
- require_relative '../hap/http_encryption'
3
-
4
1
  module RubyHome
5
2
  module HTTP
6
3
  class HAPResponse < WEBrick::HTTPResponse
7
- def initialize(*args, request_id: )
8
- @_request_id = request_id
4
+ def initialize(*args)
9
5
  cache[:accessory_to_controller_count] ||= 0
10
6
 
11
7
  super(*args)
@@ -48,10 +44,8 @@ module RubyHome
48
44
  end
49
45
 
50
46
  def cache
51
- GlobalCache.instance[@_request_id] ||= Cache.new
47
+ RequestStore.store
52
48
  end
53
49
  end
54
50
  end
55
51
  end
56
-
57
-
@@ -1,26 +1,21 @@
1
- require 'webrick/httpserver'
2
- require 'webrick/httpstatus'
3
- require_relative 'hap_request'
4
- require_relative 'hap_response'
5
-
6
1
  module RubyHome
7
2
  module HTTP
8
3
  class HAPServer < WEBrick::HTTPServer
9
- def run(sock)
4
+ def run(socket)
10
5
  while true
11
- res = RubyHome::HTTP::HAPResponse.new(@config, request_id: sock.object_id)
12
- req = RubyHome::HTTP::HAPRequest.new(@config, request_id: sock.object_id)
6
+ res = RubyHome::HTTP::HAPResponse.new(@config)
7
+ req = RubyHome::HTTP::HAPRequest.new(@config)
13
8
  server = self
14
9
  begin
15
10
  timeout = @config[:RequestTimeout]
16
11
  while timeout > 0
17
- break if sock.to_io.wait_readable(0.5)
12
+ break if socket.to_io.wait_readable(0.5)
18
13
  break if @status != :Running
19
14
  timeout -= 0.5
20
15
  end
21
16
  raise WEBrick::HTTPStatus::EOFError if timeout <= 0 || @status != :Running
22
- raise WEBrick::HTTPStatus::EOFError if sock.eof?
23
- req.parse(sock)
17
+ raise WEBrick::HTTPStatus::EOFError if socket.eof?
18
+ req.parse(socket)
24
19
  res.received_encrypted_request = req.received_encrypted_request?
25
20
  res.request_method = req.request_method
26
21
  res.request_uri = req.request_uri
@@ -50,7 +45,7 @@ module RubyHome
50
45
  if req.keep_alive? && res.keep_alive?
51
46
  req.fixup()
52
47
  end
53
- res.send_response(sock)
48
+ res.send_response(socket)
54
49
  server.access_log(@config, req, res)
55
50
  end
56
51
  end
@@ -1,5 +1,3 @@
1
- require 'oj'
2
-
3
1
  module RubyHome
4
2
  module HTTP
5
3
  module ObjectSerializer
@@ -0,0 +1,46 @@
1
+ class StartSRPService
2
+ def initialize(username: , password:)
3
+ @username = username
4
+ @password = password
5
+ end
6
+
7
+ def salt_bytes
8
+ [salt].pack('H*')
9
+ end
10
+
11
+ def public_key_bytes
12
+ [public_key].pack('H*')
13
+ end
14
+
15
+ def proof
16
+ challenge_and_proof[:proof]
17
+ end
18
+
19
+ private
20
+
21
+ def salt
22
+ user_auth[:salt]
23
+ end
24
+
25
+ def public_key
26
+ challenge[:B]
27
+ end
28
+
29
+ def challenge
30
+ challenge_and_proof[:challenge]
31
+ end
32
+
33
+ def challenge_and_proof
34
+ srp_verifier.get_challenge_and_proof(username, user_auth[:verifier], user_auth[:salt])
35
+ end
36
+
37
+ def user_auth
38
+ @_user_auth ||= srp_verifier.generate_userauth(username, password)
39
+ end
40
+
41
+ def srp_verifier
42
+ @_verifier ||= RubyHome::SRP::Verifier.new
43
+ end
44
+
45
+ attr_reader :username, :password
46
+ end
@@ -0,0 +1,55 @@
1
+ class VerifySRPService
2
+ def initialize(public_key: , device_proof: , srp_session: )
3
+ @device_proof = device_proof
4
+ @srp_session = srp_session
5
+ @public_key = public_key
6
+ end
7
+
8
+ def valid?
9
+ return false unless public_key
10
+ return false unless device_proof
11
+ return false unless srp_session
12
+ return false unless valid_session?
13
+
14
+ true
15
+ end
16
+
17
+ def session_key
18
+ srp_verifier.K
19
+ end
20
+
21
+ def server_proof
22
+ verify_session_bytes
23
+ end
24
+
25
+ private
26
+
27
+ def valid_session?
28
+ !!verify_session
29
+ end
30
+
31
+ def verify_session_bytes
32
+ [verify_session].pack('H*')
33
+ end
34
+
35
+ def verify_session
36
+ @_verify_session ||= srp_verifier.verify_session(
37
+ srp_session.merge({A: public_key_bytes}),
38
+ device_proof_bytes
39
+ )
40
+ end
41
+
42
+ def public_key_bytes
43
+ public_key.unpack1('H*')
44
+ end
45
+
46
+ def device_proof_bytes
47
+ device_proof.unpack1('H*')
48
+ end
49
+
50
+ def srp_verifier
51
+ @_verifier ||= RubyHome::SRP::Verifier.new
52
+ end
53
+
54
+ attr_reader :public_key, :device_proof, :srp_session
55
+ end
@@ -1,6 +1,3 @@
1
- require 'webrick'
2
- require_relative '../../http/hap_server'
3
-
4
1
  module RubyHome
5
2
  module Rack
6
3
  module Handler
@@ -11,10 +8,8 @@ module RubyHome
11
8
 
12
9
  options[:BindAddress] = options.delete(:Host) || default_host
13
10
  options[:Port] ||= 8080
14
- unless ENV['DEBUG']
15
- options[:Logger] = WEBrick::Log.new("/dev/null")
16
- options[:AccessLog] = []
17
- end
11
+ options[:Logger] = WEBrick::Log.new("/dev/null")
12
+ options[:AccessLog] = []
18
13
  @server = HTTP::HAPServer.new(options)
19
14
  @server.mount '/', Handler::HAPServer, app
20
15
  yield @server if block_given?
@@ -1,3 +1,3 @@
1
1
  module RubyHome
2
- VERSION = '0.1.2'
2
+ VERSION = '0.1.3'
3
3
  end