ruby_home 0.1.5 → 0.1.7

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -5
  3. data/bin/rubyhome +4 -4
  4. data/lib/ruby_home/accessory_info.rb +45 -30
  5. data/lib/ruby_home/factories/characteristic_factory.rb +27 -19
  6. data/lib/ruby_home/factories/default_values/float_value.rb +1 -1
  7. data/lib/ruby_home/factories/service_factory.rb +89 -0
  8. data/lib/ruby_home/factories/templates/characteristic_template.rb +6 -2
  9. data/lib/ruby_home/hap/accessory.rb +24 -3
  10. data/lib/ruby_home/hap/accessory_collection.rb +38 -0
  11. data/lib/ruby_home/hap/characteristic.rb +25 -19
  12. data/lib/ruby_home/hap/crypto/session_key.rb +31 -0
  13. data/lib/ruby_home/hap/ev_response.rb +64 -0
  14. data/lib/ruby_home/{http → hap}/hap_request.rb +12 -5
  15. data/lib/ruby_home/{http → hap}/hap_response.rb +7 -4
  16. data/lib/ruby_home/hap/server.rb +81 -0
  17. data/lib/ruby_home/hap/service.rb +11 -15
  18. data/lib/ruby_home/http/application.rb +17 -22
  19. data/lib/ruby_home/http/controllers/application_controller.rb +8 -4
  20. data/lib/ruby_home/http/controllers/characteristics_controller.rb +19 -4
  21. data/lib/ruby_home/http/controllers/pair_verifies_controller.rb +3 -5
  22. data/lib/ruby_home/http/serializers/object_serializer.rb +1 -1
  23. data/lib/ruby_home/http/services/socket_notifier.rb +40 -0
  24. data/lib/ruby_home/http/services/start_srp_service.rb +36 -34
  25. data/lib/ruby_home/http/services/verify_srp_service.rb +55 -53
  26. data/lib/ruby_home/identifier_cache.rb +22 -49
  27. data/lib/ruby_home/persistable.rb +36 -0
  28. data/lib/ruby_home/version.rb +1 -1
  29. data/lib/ruby_home.rb +14 -5
  30. data/rubyhome.gemspec +3 -3
  31. metadata +35 -38
  32. data/lib/ruby_home/factories/accessory_factory.rb +0 -73
  33. data/lib/ruby_home/http/hap_server.rb +0 -60
  34. data/lib/ruby_home/rack/handler/hap_server.rb +0 -21
  35. data/lib/ruby_home/yaml_record.rb +0 -440
@@ -1,10 +1,11 @@
1
1
  module RubyHome
2
- module HTTP
2
+ module HAP
3
3
  class HAPResponse < WEBrick::HTTPResponse
4
- def initialize(*args)
4
+ def initialize(config, sock)
5
+ @sock = sock
5
6
  cache[:accessory_to_controller_count] ||= 0
6
7
 
7
- super(*args)
8
+ super(config)
8
9
  end
9
10
 
10
11
  def send_response(socket)
@@ -25,6 +26,8 @@ module RubyHome
25
26
 
26
27
  private
27
28
 
29
+ attr_reader :sock
30
+
28
31
  def encrypter
29
32
  @_encrypter ||= RubyHome::HAP::HTTPEncryption.new(encryption_key, encrypter_params)
30
33
  end
@@ -44,7 +47,7 @@ module RubyHome
44
47
  end
45
48
 
46
49
  def cache
47
- RequestStore.store
50
+ RubyHome.socket_store[sock]
48
51
  end
49
52
  end
50
53
  end
@@ -0,0 +1,81 @@
1
+ module RubyHome
2
+ module HAP
3
+ class Server
4
+ def initialize(host, port, socket_store)
5
+ @port = port
6
+ @host = host
7
+ @socket_store = socket_store
8
+ @selector = NIO::Selector.new
9
+ end
10
+
11
+ attr_reader :port, :host, :socket_store
12
+
13
+ def run
14
+ puts "Listening on #{host}:#{port}"
15
+ @server = TCPServer.new(host, port)
16
+
17
+ monitor = @selector.register(@server, :r)
18
+ monitor.value = proc { accept }
19
+
20
+ loop do
21
+ @selector.select { |monitor| monitor.value.call(monitor) }
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def accept
28
+ socket = @server.accept
29
+ _, port, host = socket.peeraddr
30
+ puts "*** #{host}:#{port} connected"
31
+
32
+ monitor = @selector.register(socket, :r)
33
+ monitor.value = proc { read(socket) }
34
+ end
35
+
36
+ def upstream
37
+ @_upstream ||= HTTP::Application.run
38
+ end
39
+
40
+ def read(socket)
41
+ return close(socket) if socket.eof?
42
+
43
+ socket_store[socket] ||= {}
44
+
45
+ request = HAPRequest.new(WEBrick::Config::HTTP, socket)
46
+ response = HAPResponse.new(WEBrick::Config::HTTP, socket)
47
+
48
+ request.parse(socket)
49
+
50
+ response.received_encrypted_request = request.received_encrypted_request?
51
+ response.request_method = request.request_method
52
+ response.request_uri = request.request_uri
53
+ response.request_http_version = request.http_version
54
+ response.keep_alive = request.keep_alive?
55
+
56
+ upstream.service(request, response)
57
+
58
+ if request.request_line
59
+ if request.keep_alive? && response.keep_alive?
60
+ request.fixup()
61
+ end
62
+ response.send_response(socket)
63
+ end
64
+
65
+ return close(socket) unless request.keep_alive?
66
+ return close(socket) unless response.keep_alive?
67
+ rescue EOFError
68
+ close(socket)
69
+ end
70
+
71
+ def close(socket)
72
+ _, port, host = socket.peeraddr
73
+ puts "*** #{host}:#{port} disconnected"
74
+ @selector.deregister(socket)
75
+ socket_store.delete(socket)
76
+ socket.close
77
+ end
78
+ end
79
+ end
80
+ end
81
+
@@ -8,28 +8,24 @@ module RubyHome
8
8
  @description = description
9
9
  @uuid = uuid
10
10
  @characteristics = []
11
+ @instance_id = accessory.next_available_instance_id
11
12
  end
12
13
 
13
- attr_reader :accessory, :characteristics, :primary, :hidden, :name, :description, :uuid
14
- attr_accessor :instance_id
14
+ attr_reader(
15
+ :accessory,
16
+ :characteristics,
17
+ :primary,
18
+ :hidden,
19
+ :name,
20
+ :description,
21
+ :uuid,
22
+ :instance_id
23
+ )
15
24
 
16
25
  def characteristic(characteristic_name)
17
26
  characteristics.find do |characteristic|
18
27
  characteristic.name == characteristic_name
19
28
  end
20
29
  end
21
-
22
- def save
23
- IdentifierCache.add_accessory(accessory)
24
- IdentifierCache.add_service(self)
25
- end
26
-
27
- def inspect
28
- {
29
- primary: primary,
30
- hidden: hidden,
31
- characteristics: characteristics
32
- }
33
- end
34
30
  end
35
31
  end
@@ -1,31 +1,26 @@
1
- Dir[File.dirname(__FILE__) + '/controllers/*.rb'].each {|file| require file }
1
+ Dir[File.dirname(__FILE__) + '/controllers/*.rb'].each { |file| require file }
2
2
 
3
3
  module RubyHome
4
4
  module HTTP
5
5
  class Application
6
- def run
7
- RubyHome::Rack::Handler::HAPServer.run rack_builder,
8
- Port: port,
9
- Host: bind_address,
10
- ServerSoftware: 'RubyHome'
11
- end
12
-
13
- def port
14
- @_port ||= Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567)
15
- end
6
+ class << self
7
+ def run
8
+ ::Rack::Handler::WEBrick.new(
9
+ ::WEBrick::HTTPServer.new(DoNotListen: true),
10
+ rack_builder
11
+ )
12
+ end
16
13
 
17
- def bind_address
18
- '0.0.0.0'
19
- end
14
+ def rack_builder
15
+ ::Rack::Builder.new do
16
+ use ::Rack::CommonLogger
20
17
 
21
- def rack_builder
22
- ::Rack::Builder.new do
23
- use ::Rack::CommonLogger
24
- map('/accessories', &Proc.new { run AccessoriesController })
25
- map('/characteristics', &Proc.new { run CharacteristicsController })
26
- map('/pair-setup', &Proc.new { run PairSetupsController })
27
- map('/pair-verify', &Proc.new { run PairVerifiesController })
28
- map('/pairings', &Proc.new { run PairingsController })
18
+ map('/accessories') { run AccessoriesController }
19
+ map('/characteristics') { run CharacteristicsController }
20
+ map('/pair-setup') { run PairSetupsController }
21
+ map('/pair-verify') { run PairVerifiesController }
22
+ map('/pairings') { run PairingsController }
23
+ end
29
24
  end
30
25
  end
31
26
  end
@@ -28,15 +28,19 @@ module RubyHome
28
28
  end
29
29
 
30
30
  def identifier_cache
31
- IdentifierCache
31
+ Accessory.all
32
32
  end
33
33
 
34
- def cache
35
- RequestStore.store
34
+ def socket
35
+ env["REQUEST_SOCKET"]
36
36
  end
37
37
 
38
38
  def clear_cache
39
- RequestStore.clear!
39
+ RubyHome.socket_store.delete(socket)
40
+ end
41
+
42
+ def cache
43
+ RubyHome.socket_store[socket]
40
44
  end
41
45
 
42
46
  def tlv(object)
@@ -14,7 +14,11 @@ module RubyHome
14
14
 
15
15
  put '/' do
16
16
  json_body.fetch('characteristics', []).each do |characteristic_params|
17
- update_characteristics(characteristic_params)
17
+ if characteristic_params['value']
18
+ update_characteristics(characteristic_params)
19
+ elsif characteristic_params['ev']
20
+ subscribe_characteristics(characteristic_params)
21
+ end
18
22
  end
19
23
 
20
24
  status 204
@@ -30,14 +34,25 @@ module RubyHome
30
34
  end
31
35
 
32
36
  def update_characteristics(characteristic_params)
33
- characteristic = find_characteristic(**characteristic_params.symbolize_keys.slice(:aid, :iid))
34
- if characteristic && characteristic_params['value']
37
+ find_characteristic(**characteristic_params.symbolize_keys.slice(:aid, :iid)) do |characteristic|
35
38
  characteristic.value = characteristic_params['value']
36
39
  end
37
40
  end
38
41
 
42
+ def subscribe_characteristics(characteristic_params)
43
+ find_characteristic(**characteristic_params.symbolize_keys.slice(:aid, :iid)) do |characteristic|
44
+ notifier = SocketNotifier.new(socket, characteristic)
45
+
46
+ unless characteristic.listeners.include?(notifier)
47
+ characteristic.subscribe(notifier)
48
+ end
49
+ end
50
+ end
51
+
39
52
  def find_characteristic(aid:, iid:)
40
- IdentifierCache.find_characteristic(accessory_id: aid.to_i, instance_id: iid.to_i)
53
+ characteristic = identifier_cache.find_characteristic(accessory_id: aid.to_i, instance_id: iid.to_i)
54
+ yield characteristic if block_given?
55
+ characteristic
41
56
  end
42
57
 
43
58
  def require_session
@@ -56,11 +56,9 @@ module RubyHome
56
56
  unpacked_decrypted_data = HAP::TLV.read(decrypted_data)
57
57
 
58
58
  if accessory_info.paired_clients.any? {|h| h[:identifier] == unpacked_decrypted_data[:identifier]}
59
- hkdf = HAP::Crypto::HKDF.new(info: 'Control-Write-Encryption-Key', salt: 'Control-Salt')
60
- cache[:controller_to_accessory_key] = hkdf.encrypt(cache[:shared_secret])
61
-
62
- hkdf = HAP::Crypto::HKDF.new(info: 'Control-Read-Encryption-Key', salt: 'Control-Salt')
63
- cache[:accessory_to_controller_key] = hkdf.encrypt(cache[:shared_secret])
59
+ shared_secret = HAP::Crypto::SessionKey.new(cache[:shared_secret])
60
+ cache[:controller_to_accessory_key] = shared_secret.controller_to_accessory_key
61
+ cache[:accessory_to_controller_key] = shared_secret.accessory_to_controller_key
64
62
 
65
63
  cache.delete(:session_key)
66
64
  cache.delete(:shared_secret)
@@ -15,7 +15,7 @@ module RubyHome
15
15
 
16
16
  def serializable_hash
17
17
  serializable_hash = if @resource
18
- record_hash(resource)
18
+ record_hash(@resource)
19
19
  elsif @resources
20
20
  @resources.map do |resource|
21
21
  record_hash(resource)
@@ -0,0 +1,40 @@
1
+ module RubyHome
2
+ module HTTP
3
+ class SocketNotifier
4
+ def initialize(socket, characteristic)
5
+ @socket = socket
6
+ @characteristic = characteristic
7
+ end
8
+
9
+ def updated(_)
10
+ if socket_still_active?
11
+ send_ev_response
12
+ else
13
+ characteristic.unsubscribe(self)
14
+ end
15
+ end
16
+
17
+ def ==(other)
18
+ self.class == other.class &&
19
+ self.socket == other.socket &&
20
+ self.characteristic == other.characteristic
21
+ end
22
+
23
+ attr_reader :socket, :characteristic
24
+
25
+ private
26
+
27
+ def socket_still_active?
28
+ RubyHome.socket_store.include?(socket)
29
+ end
30
+
31
+ def send_ev_response
32
+ HAP::EVResponse.new(socket, serialized_characteristic).send_response
33
+ end
34
+
35
+ def serialized_characteristic
36
+ HTTP::CharacteristicValueSerializer.new([characteristic]).serialized_json
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,46 +1,48 @@
1
- class StartSRPService
2
- def initialize(username: , password:)
3
- @username = username
4
- @password = password
5
- end
1
+ module RubyHome
2
+ class StartSRPService
3
+ def initialize(username: , password:)
4
+ @username = username
5
+ @password = password
6
+ end
6
7
 
7
- def salt_bytes
8
- [salt].pack('H*')
9
- end
8
+ def salt_bytes
9
+ [salt].pack('H*')
10
+ end
10
11
 
11
- def public_key_bytes
12
- [public_key].pack('H*')
13
- end
12
+ def public_key_bytes
13
+ [public_key].pack('H*')
14
+ end
14
15
 
15
- def proof
16
- challenge_and_proof[:proof]
17
- end
16
+ def proof
17
+ challenge_and_proof[:proof]
18
+ end
18
19
 
19
- private
20
+ private
20
21
 
21
- def salt
22
- user_auth[:salt]
23
- end
22
+ def salt
23
+ user_auth[:salt]
24
+ end
24
25
 
25
- def public_key
26
- challenge[:B]
27
- end
26
+ def public_key
27
+ challenge[:B]
28
+ end
28
29
 
29
- def challenge
30
- challenge_and_proof[:challenge]
31
- end
30
+ def challenge
31
+ challenge_and_proof[:challenge]
32
+ end
32
33
 
33
- def challenge_and_proof
34
- srp_verifier.get_challenge_and_proof(username, user_auth[:verifier], user_auth[:salt])
35
- end
34
+ def challenge_and_proof
35
+ srp_verifier.get_challenge_and_proof(username, user_auth[:verifier], user_auth[:salt])
36
+ end
36
37
 
37
- def user_auth
38
- @_user_auth ||= srp_verifier.generate_userauth(username, password)
39
- end
38
+ def user_auth
39
+ @_user_auth ||= srp_verifier.generate_userauth(username, password)
40
+ end
40
41
 
41
- def srp_verifier
42
- @_verifier ||= RubyHome::SRP::Verifier.new
43
- end
42
+ def srp_verifier
43
+ @_verifier ||= RubyHome::SRP::Verifier.new
44
+ end
44
45
 
45
- attr_reader :username, :password
46
+ attr_reader :username, :password
47
+ end
46
48
  end
@@ -1,55 +1,57 @@
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
1
+ module RubyHome
2
+ class VerifySRPService
3
+ def initialize(public_key: , device_proof: , srp_session: )
4
+ @device_proof = device_proof
5
+ @srp_session = srp_session
6
+ @public_key = public_key
7
+ end
8
+
9
+ def valid?
10
+ return false unless public_key
11
+ return false unless device_proof
12
+ return false unless srp_session
13
+ return false unless valid_session?
14
+
15
+ true
16
+ end
17
+
18
+ def session_key
19
+ srp_verifier.K
20
+ end
21
+
22
+ def server_proof
23
+ verify_session_bytes
24
+ end
25
+
26
+ private
27
+
28
+ def valid_session?
29
+ !!verify_session
30
+ end
31
+
32
+ def verify_session_bytes
33
+ [verify_session].pack('H*')
34
+ end
35
+
36
+ def verify_session
37
+ @_verify_session ||= srp_verifier.verify_session(
38
+ srp_session.merge({A: public_key_bytes}),
39
+ device_proof_bytes
40
+ )
41
+ end
42
+
43
+ def public_key_bytes
44
+ public_key.unpack1('H*')
45
+ end
46
+
47
+ def device_proof_bytes
48
+ device_proof.unpack1('H*')
49
+ end
50
+
51
+ def srp_verifier
52
+ @_verifier ||= RubyHome::SRP::Verifier.new
53
+ end
54
+
55
+ attr_reader :public_key, :device_proof, :srp_session
6
56
  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
57
  end
@@ -1,59 +1,32 @@
1
+ require_relative 'persistable'
2
+
1
3
  module RubyHome
2
4
  class IdentifierCache
3
- class << self
4
- attr_accessor :accessories
5
-
6
- def accessories
7
- @@accessories ||= []
8
- end
9
-
10
- def reset!
11
- @@accessories = []
12
- end
13
-
14
- def services
15
- accessories.flat_map(&:services)
16
- end
17
-
18
- def characteristics
19
- services.flat_map(&:characteristics)
20
- end
21
-
22
- def find_characteristic(attributes)
23
- characteristics.find do |characteristic|
24
- attributes.all? do |key, value|
25
- characteristic.send(key) == value
26
- end
27
- end
28
- end
29
-
30
- def add_accessory(accessory)
31
- return true if accessories.include?(accessory)
5
+ include Persistable
32
6
 
33
- accessories << accessory.tap do |a|
34
- a.id = accessories.size + 1
35
- end
7
+ self.source = 'identifier_cache.yml'
36
8
 
37
- end
38
-
39
- def add_service(service)
40
- return true if services.include?(service)
41
-
42
- accessory = service.accessory
43
- accessory.services << service.tap do |s|
44
- s.instance_id = accessory.next_available_instance_id
45
- end
9
+ def self.instance
10
+ @@_instance ||= persisted || create
11
+ end
46
12
 
47
- end
13
+ def self.reload
14
+ @@_instance = nil
15
+ end
48
16
 
49
- def add_characteristic(characteristic)
50
- return true if characteristics.include?(characteristic)
17
+ def initialize(accessory_id: , instance_id: , uuid: )
18
+ @accessory_id = accessory_id
19
+ @instance_id = instance_id
20
+ @uuid = uuid
21
+ end
51
22
 
52
- service = characteristic.service
53
- service.characteristics << characteristic.tap do |c|
54
- c.instance_id = characteristic.accessory.next_available_instance_id
55
- end
56
- end
23
+ def persisted_attributes
24
+ {
25
+ accessory_id: accessory.id,
26
+ instance_id: instance.instance_id,
27
+ uuid: instance.uuid
28
+ }
57
29
  end
58
30
  end
59
31
  end
32
+
@@ -0,0 +1,36 @@
1
+ module RubyHome
2
+ module Persistable
3
+ def self.included(base)
4
+ base.send(:cattr_accessor, :source)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def persisted
10
+ new(read) if read
11
+ end
12
+
13
+ def create(**options)
14
+ new(**options).tap(&:save)
15
+ end
16
+
17
+ def write(collection)
18
+ File.open(source, 'w') {|f| f.write(collection.to_yaml) }
19
+ end
20
+
21
+ def read
22
+ return false unless File.exists?(source)
23
+
24
+ YAML.load_file(source)
25
+ end
26
+ end
27
+
28
+ def reload
29
+ self.class.reload
30
+ end
31
+
32
+ def save
33
+ self.class.write(persisted_attributes)
34
+ end
35
+ end
36
+ end
@@ -1,3 +1,3 @@
1
1
  module RubyHome
2
- VERSION = '0.1.5'
2
+ VERSION = '0.1.7'
3
3
  end