ruby_home 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
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