selfsdk 0.0.124
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.
- checksums.yaml +7 -0
- data/lib/acl.rb +52 -0
- data/lib/authenticated.rb +26 -0
- data/lib/client.rb +95 -0
- data/lib/jwt_service.rb +91 -0
- data/lib/log.rb +15 -0
- data/lib/messages/attestation.rb +53 -0
- data/lib/messages/authentication_message.rb +25 -0
- data/lib/messages/authentication_req.rb +52 -0
- data/lib/messages/authentication_resp.rb +23 -0
- data/lib/messages/base.rb +89 -0
- data/lib/messages/fact.rb +55 -0
- data/lib/messages/fact_request.rb +112 -0
- data/lib/messages/fact_response.rb +91 -0
- data/lib/messages/message.rb +39 -0
- data/lib/messaging.rb +441 -0
- data/lib/ntptime.rb +51 -0
- data/lib/proto/acl_pb.rb +19 -0
- data/lib/proto/aclcommand_pb.rb +16 -0
- data/lib/proto/auth_pb.rb +19 -0
- data/lib/proto/errtype_pb.rb +19 -0
- data/lib/proto/header_pb.rb +16 -0
- data/lib/proto/message_pb.rb +22 -0
- data/lib/proto/msgtype_pb.rb +18 -0
- data/lib/proto/notification_pb.rb +19 -0
- data/lib/selfsdk.rb +109 -0
- data/lib/services/auth.rb +163 -0
- data/lib/services/facts.rb +138 -0
- data/lib/services/identity.rb +44 -0
- data/lib/services/messaging.rb +100 -0
- data/lib/sources.rb +78 -0
- metadata +354 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SelfSDK
|
4
|
+
module Messages
|
5
|
+
class Base
|
6
|
+
attr_accessor :from, :from_device, :to, :to_device, :expires, :id,
|
7
|
+
:fields, :typ, :payload, :status, :input, :intermediary,
|
8
|
+
:description, :sub, :exp_timeout
|
9
|
+
|
10
|
+
def initialize(messaging)
|
11
|
+
@client = messaging.client
|
12
|
+
@jwt = @client.jwt
|
13
|
+
@messaging = messaging
|
14
|
+
@device_id = "1"
|
15
|
+
end
|
16
|
+
|
17
|
+
def request
|
18
|
+
res = @messaging.send_and_wait_for_response(proto, self)
|
19
|
+
SelfSDK.logger.info "synchronously messaging to #{@to}:#{@to_device}"
|
20
|
+
res
|
21
|
+
end
|
22
|
+
|
23
|
+
def send_message
|
24
|
+
res = @messaging.send_message proto
|
25
|
+
SelfSDK.logger.info "asynchronously requested information to #{@to}:#{@to_device}"
|
26
|
+
res
|
27
|
+
end
|
28
|
+
|
29
|
+
def unauthorized?
|
30
|
+
status == "unauthorized"
|
31
|
+
end
|
32
|
+
|
33
|
+
def rejected?
|
34
|
+
status == "rejected"
|
35
|
+
end
|
36
|
+
|
37
|
+
def accepted?
|
38
|
+
status == "accepted"
|
39
|
+
end
|
40
|
+
|
41
|
+
def errored?
|
42
|
+
status == "errored"
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate!(original)
|
46
|
+
unless original.nil?
|
47
|
+
raise ::StandardError.new("bad response audience") if @audience != original.from
|
48
|
+
if original.intermediary.nil?
|
49
|
+
raise ::StandardError.new("bad issuer") if @from != original.to
|
50
|
+
else
|
51
|
+
raise ::StandardError.new("bad issuer") if @from != original.intermediary
|
52
|
+
end
|
53
|
+
end
|
54
|
+
raise ::StandardError.new("expired message") if @expires < SelfSDK::Time.now
|
55
|
+
raise ::StandardError.new("issued too soon") if @issued > SelfSDK::Time.now
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def proto
|
61
|
+
raise ::StandardError.new("must define this method")
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def get_payload(input)
|
67
|
+
body = if input.is_a? String
|
68
|
+
input
|
69
|
+
else
|
70
|
+
input.ciphertext
|
71
|
+
end
|
72
|
+
|
73
|
+
jwt = JSON.parse(body, symbolize_names: true)
|
74
|
+
payload = JSON.parse(@jwt.decode(jwt[:payload]), symbolize_names: true)
|
75
|
+
@from = payload[:iss]
|
76
|
+
verify! jwt
|
77
|
+
payload
|
78
|
+
end
|
79
|
+
|
80
|
+
def verify!(jwt)
|
81
|
+
k = @client.public_keys(@from).first[:key]
|
82
|
+
return if @jwt.verify(jwt, k)
|
83
|
+
|
84
|
+
SelfSDK.logger.info "skipping message, invalid signature"
|
85
|
+
raise ::StandardError.new("invalid signature on incoming message")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'attestation'
|
4
|
+
|
5
|
+
module SelfSDK
|
6
|
+
module Messages
|
7
|
+
class Fact
|
8
|
+
attr_accessor :name, :attestations, :operator, :expected_value, :sources
|
9
|
+
|
10
|
+
def initialize(messaging)
|
11
|
+
@messaging = messaging
|
12
|
+
end
|
13
|
+
|
14
|
+
def parse(fact)
|
15
|
+
@name = SelfSDK::fact_name(fact[:fact])
|
16
|
+
|
17
|
+
@operator = ""
|
18
|
+
@operator = SelfSDK::operator(fact[:operator]) if fact[:operator]
|
19
|
+
|
20
|
+
@sources = []
|
21
|
+
fact[:sources]&.each do |s|
|
22
|
+
@sources << SelfSDK::source(s)
|
23
|
+
end
|
24
|
+
|
25
|
+
@expected_value = fact[:expected_value] || ""
|
26
|
+
@attestations = []
|
27
|
+
|
28
|
+
fact[:attestations]&.each do |a|
|
29
|
+
attestation = SelfSDK::Messages::Attestation.new(@messaging)
|
30
|
+
attestation.parse(fact[:fact].to_sym, a)
|
31
|
+
@attestations.push(attestation)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate!(original)
|
36
|
+
@attestations.each do |a|
|
37
|
+
a.validate! original
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_hash
|
42
|
+
h = { fact: @name }
|
43
|
+
unless @sources.nil?
|
44
|
+
h[:sources] = @sources if @sources.length > 0
|
45
|
+
end
|
46
|
+
h[:operator] = @operator unless @operator.empty?
|
47
|
+
unless @attestations.nil?
|
48
|
+
h[:attestations] = @attestations if @attestations.length > 0
|
49
|
+
end
|
50
|
+
h[:expected_value] = @expected_value unless @expected_value.empty?
|
51
|
+
h
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative '../ntptime'
|
5
|
+
|
6
|
+
module SelfSDK
|
7
|
+
module Messages
|
8
|
+
class FactRequest < Base
|
9
|
+
MSG_TYPE = "identities.facts.query.req"
|
10
|
+
DEFAULT_EXP_TIMEOUT = 900
|
11
|
+
|
12
|
+
attr_accessor :facts, :options
|
13
|
+
|
14
|
+
def parse_facts(facts)
|
15
|
+
@facts = []
|
16
|
+
facts.each do |fact|
|
17
|
+
f = SelfSDK::Messages::Fact.new(@messaging)
|
18
|
+
f.parse(fact)
|
19
|
+
@facts << f.to_hash
|
20
|
+
end
|
21
|
+
@facts
|
22
|
+
end
|
23
|
+
|
24
|
+
def populate(selfid, facts, opts)
|
25
|
+
@id = SecureRandom.uuid
|
26
|
+
@from = @client.jwt.id
|
27
|
+
@to = selfid
|
28
|
+
@facts = parse_facts(facts)
|
29
|
+
|
30
|
+
@id = opts[:cid] if opts.include?(:cid)
|
31
|
+
@options = opts.fetch(:options, false)
|
32
|
+
@description = opts.include?(:description) ? opts[:description] : nil
|
33
|
+
@exp_timeout = opts.fetch(:exp_timeout, DEFAULT_EXP_TIMEOUT)
|
34
|
+
|
35
|
+
@intermediary = if opts.include?(:intermediary)
|
36
|
+
opts[:intermediary]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse(input)
|
41
|
+
@input = input
|
42
|
+
@typ = MSG_TYPE
|
43
|
+
@payload = get_payload input
|
44
|
+
@id = @payload[:cid]
|
45
|
+
@from = @payload[:iss]
|
46
|
+
@to = @payload[:sub]
|
47
|
+
@expires = @payload[:exp]
|
48
|
+
@description = @payload.include?(:description) ? @payload[:description] : nil
|
49
|
+
@facts = @payload[:facts]
|
50
|
+
@options = @payload[:options]
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_response
|
54
|
+
m = SelfSDK::Messages::FactResponse.new(@messaging)
|
55
|
+
m.id = @id
|
56
|
+
m.from = @to
|
57
|
+
m.to = @from
|
58
|
+
m.audience = @from
|
59
|
+
m.to_device = @messaging.device_id
|
60
|
+
m.facts = @facts
|
61
|
+
m
|
62
|
+
end
|
63
|
+
|
64
|
+
def share_facts(facts)
|
65
|
+
m = build_response
|
66
|
+
m.facts = parse_facts(facts)
|
67
|
+
m.send_message
|
68
|
+
end
|
69
|
+
|
70
|
+
def body
|
71
|
+
b = {
|
72
|
+
typ: MSG_TYPE,
|
73
|
+
iss: @jwt.id,
|
74
|
+
sub: @to,
|
75
|
+
iat: SelfSDK::Time.now.strftime('%FT%TZ'),
|
76
|
+
exp: (SelfSDK::Time.now + @exp_timeout).strftime('%FT%TZ'),
|
77
|
+
cid: @id,
|
78
|
+
jti: SecureRandom.uuid,
|
79
|
+
facts: @facts,
|
80
|
+
}
|
81
|
+
b[:options] = @options unless (@options.nil? || @options == false)
|
82
|
+
b[:description] = @description unless (@description.nil? || @description.empty?)
|
83
|
+
b
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def proto
|
89
|
+
devices = if @intermediary.nil?
|
90
|
+
@client.devices(@to)
|
91
|
+
else
|
92
|
+
@client.devices(@intermediary)
|
93
|
+
end
|
94
|
+
@to_device = devices.first
|
95
|
+
|
96
|
+
recipient = if @intermediary.nil?
|
97
|
+
"#{@to}:#{@to_device}"
|
98
|
+
else
|
99
|
+
"#{@intermediary}:#{@to_device}"
|
100
|
+
end
|
101
|
+
|
102
|
+
Msgproto::Message.new(
|
103
|
+
type: Msgproto::MsgType::MSG,
|
104
|
+
id: @id,
|
105
|
+
sender: "#{@jwt.id}:#{@messaging.device_id}",
|
106
|
+
recipient: recipient,
|
107
|
+
ciphertext: @jwt.prepare(body),
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require_relative 'fact'
|
5
|
+
require_relative '../ntptime'
|
6
|
+
|
7
|
+
module SelfSDK
|
8
|
+
module Messages
|
9
|
+
class FactResponse < Base
|
10
|
+
MSG_TYPE = "identities.facts.query.resp"
|
11
|
+
|
12
|
+
attr_accessor :facts, :audience
|
13
|
+
|
14
|
+
def parse(input)
|
15
|
+
@input = input
|
16
|
+
@typ = MSG_TYPE
|
17
|
+
@payload = get_payload input
|
18
|
+
@id = payload[:cid]
|
19
|
+
@from = payload[:iss]
|
20
|
+
@to = payload[:sub]
|
21
|
+
@expires = ::Time.parse(payload[:exp])
|
22
|
+
@issued = ::Time.parse(payload[:iat])
|
23
|
+
@audience = payload[:aud]
|
24
|
+
@status = payload[:status]
|
25
|
+
@facts = []
|
26
|
+
payload[:facts] = [] if payload[:facts].nil?
|
27
|
+
payload[:facts].each do |f|
|
28
|
+
begin
|
29
|
+
fact = SelfSDK::Messages::Fact.new(@messaging)
|
30
|
+
fact.parse(f)
|
31
|
+
@facts.push(fact)
|
32
|
+
rescue StandardError => e
|
33
|
+
SelfSDK.logger.info e.message
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def fact(name)
|
39
|
+
name = SelfSDK::fact_name(name)
|
40
|
+
@facts.select{|f| f.name == name}.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def attestations_for(name)
|
44
|
+
f = fact(name)
|
45
|
+
return [] if f.nil?
|
46
|
+
f.attestations
|
47
|
+
end
|
48
|
+
|
49
|
+
def attestation_values_for(name)
|
50
|
+
a = attestations_for(name)
|
51
|
+
a.map{|a| a.value}
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate!(original)
|
55
|
+
super
|
56
|
+
@facts.each do |f|
|
57
|
+
f.validate! original
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def proto
|
64
|
+
encoded_facts = []
|
65
|
+
@facts.each do |fact|
|
66
|
+
encoded_facts.push(fact.to_hash)
|
67
|
+
end
|
68
|
+
body = @jwt.prepare(
|
69
|
+
typ: MSG_TYPE,
|
70
|
+
iss: @jwt.id,
|
71
|
+
sub: @sub || @to,
|
72
|
+
aud: @audience,
|
73
|
+
iat: SelfSDK::Time.now.strftime('%FT%TZ'),
|
74
|
+
exp: (SelfSDK::Time.now + 3600).strftime('%FT%TZ'),
|
75
|
+
cid: @id,
|
76
|
+
jti: SecureRandom.uuid,
|
77
|
+
status: @status,
|
78
|
+
facts: encoded_facts,
|
79
|
+
)
|
80
|
+
|
81
|
+
Msgproto::Message.new(
|
82
|
+
type: Msgproto::MsgType::MSG,
|
83
|
+
id: SecureRandom.uuid,
|
84
|
+
sender: "#{@jwt.id}:#{@messaging.device_id}",
|
85
|
+
recipient: "#{@to}:#{@to_device}",
|
86
|
+
ciphertext: body,
|
87
|
+
)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "fact_request"
|
4
|
+
require_relative "fact_response"
|
5
|
+
require_relative "authentication_resp"
|
6
|
+
require_relative "authentication_req"
|
7
|
+
|
8
|
+
module SelfSDK
|
9
|
+
module Messages
|
10
|
+
def self.parse(input, messaging, original=nil)
|
11
|
+
body = if input.is_a? String
|
12
|
+
input
|
13
|
+
else
|
14
|
+
input.ciphertext
|
15
|
+
end
|
16
|
+
|
17
|
+
jwt = JSON.parse(body, symbolize_names: true)
|
18
|
+
payload = JSON.parse(messaging.jwt.decode(jwt[:payload]), symbolize_names: true)
|
19
|
+
|
20
|
+
case payload[:typ]
|
21
|
+
when "identities.facts.query.req"
|
22
|
+
m = FactRequest.new(messaging)
|
23
|
+
m.parse(body)
|
24
|
+
when "identities.facts.query.resp"
|
25
|
+
m = FactResponse.new(messaging)
|
26
|
+
m.parse(body)
|
27
|
+
when "identities.authenticate.resp"
|
28
|
+
m = AuthenticationResp.new(messaging)
|
29
|
+
m.parse(body)
|
30
|
+
when "identities.authenticate.req"
|
31
|
+
m = AuthenticationReq.new(messaging)
|
32
|
+
m.parse(body)
|
33
|
+
else
|
34
|
+
raise StandardError.new("Invalid message type.")
|
35
|
+
end
|
36
|
+
return m
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/messaging.rb
ADDED
@@ -0,0 +1,441 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'monitor'
|
5
|
+
require 'faye/websocket'
|
6
|
+
require 'fileutils'
|
7
|
+
require_relative 'messages/message'
|
8
|
+
require_relative 'proto/auth_pb'
|
9
|
+
require_relative 'proto/message_pb'
|
10
|
+
require_relative 'proto/msgtype_pb'
|
11
|
+
require_relative 'proto/acl_pb'
|
12
|
+
require_relative 'proto/aclcommand_pb'
|
13
|
+
|
14
|
+
module SelfSDK
|
15
|
+
class MessagingClient
|
16
|
+
DEFAULT_DEVICE="1"
|
17
|
+
DEFAULT_AUTO_RECONNECT=true
|
18
|
+
DEFAULT_STORAGE_DIR="./.self_storage"
|
19
|
+
ON_DEMAND_CLOSE_CODE=3999
|
20
|
+
|
21
|
+
attr_accessor :client, :jwt, :device_id, :ack_timeout, :timeout, :type_observer, :uuid_observer
|
22
|
+
|
23
|
+
# RestClient initializer
|
24
|
+
#
|
25
|
+
# @param url [string] self-messaging url
|
26
|
+
# @params client [Object] SelfSDK::Client object
|
27
|
+
# @option opts [string] :storage_dir the folder where encryption sessions and settings will be stored
|
28
|
+
# @option opts [Bool] :auto_reconnect Automatically reconnects to websocket if connection is lost (defaults to true).
|
29
|
+
# @option opts [String] :device_id The device id to be used by the app defaults to "1".
|
30
|
+
def initialize(url, client, options = {})
|
31
|
+
@mon = Monitor.new
|
32
|
+
@url = url
|
33
|
+
@messages = {}
|
34
|
+
@acks = {}
|
35
|
+
@type_observer = {}
|
36
|
+
@uuid_observer = {}
|
37
|
+
@jwt = client.jwt
|
38
|
+
@client = client
|
39
|
+
@ack_timeout = 60 # seconds
|
40
|
+
@timeout = 120 # seconds
|
41
|
+
@device_id = options.fetch(:device_id, DEFAULT_DEVICE)
|
42
|
+
@auto_reconnect = options.fetch(:auto_reconnect, DEFAULT_AUTO_RECONNECT)
|
43
|
+
@storage_dir = options.fetch(:storage_dir, DEFAULT_STORAGE_DIR)
|
44
|
+
@offset_file = "#{@storage_dir}/#{@jwt.id}:#{@device_id}.offset"
|
45
|
+
@offset = read_offset
|
46
|
+
|
47
|
+
FileUtils.mkdir_p @storage_dir unless File.exists? @storage_dir
|
48
|
+
|
49
|
+
if options.include? :ws
|
50
|
+
@ws = options[:ws]
|
51
|
+
else
|
52
|
+
start
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def stop
|
57
|
+
@acks.each do |k, _v|
|
58
|
+
mark_as_acknowledged(k)
|
59
|
+
end
|
60
|
+
@messages.each do |k, _v|
|
61
|
+
mark_as_acknowledged(k)
|
62
|
+
mark_as_arrived(k)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def close
|
67
|
+
@ws.close(ON_DEMAND_CLOSE_CODE, "connection closed by the client")
|
68
|
+
end
|
69
|
+
|
70
|
+
# Responds a request information request
|
71
|
+
#
|
72
|
+
# @param recipient [string] selfID to be requested
|
73
|
+
# @param recipient_device [string] device id for the selfID to be requested
|
74
|
+
# @param request [string] original message requesing information
|
75
|
+
def share_information(recipient, recipient_device, request)
|
76
|
+
send_message Msgproto::Message.new(
|
77
|
+
type: Msgproto::MsgType::MSG,
|
78
|
+
id: SecureRandom.uuid,
|
79
|
+
sender: "#{@jwt.id}:#{@device_id}",
|
80
|
+
recipient: "#{recipient}:#{recipient_device}",
|
81
|
+
ciphertext: @jwt.prepare(request),
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Send custom mmessage
|
86
|
+
#
|
87
|
+
# @param recipient [string] selfID to be requested
|
88
|
+
# @param type [string] message type
|
89
|
+
# @param request [hash] original message requesing information
|
90
|
+
def send_custom(recipient, request_body)
|
91
|
+
@to_device = @client.devices(recipient).first
|
92
|
+
send_message msg = Msgproto::Message.new(
|
93
|
+
type: Msgproto::MsgType::MSG,
|
94
|
+
id: SecureRandom.uuid,
|
95
|
+
sender: "#{@jwt.id}:#{@device_id}",
|
96
|
+
recipient: "#{recipient}:#{@to_device}",
|
97
|
+
ciphertext: @jwt.prepare(request_body),
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Allows incomming messages from the given identity
|
102
|
+
#
|
103
|
+
# @params payload [string] base64 encoded payload to be sent
|
104
|
+
def add_acl_rule(payload)
|
105
|
+
send_message Msgproto::AccessControlList.new(
|
106
|
+
type: Msgproto::MsgType::ACL,
|
107
|
+
id: SecureRandom.uuid,
|
108
|
+
command: Msgproto::ACLCommand::PERMIT,
|
109
|
+
payload: payload,
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Blocks incoming messages from specified identities
|
114
|
+
#
|
115
|
+
# @params payload [string] base64 encoded payload to be sent
|
116
|
+
def remove_acl_rule(payload)
|
117
|
+
send_message Msgproto::AccessControlList.new(
|
118
|
+
type: Msgproto::MsgType::ACL,
|
119
|
+
id: SecureRandom.uuid,
|
120
|
+
command: Msgproto::ACLCommand::REVOKE,
|
121
|
+
payload: payload,
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Lists acl rules
|
126
|
+
def list_acl_rules
|
127
|
+
wait_for 'acl_list' do
|
128
|
+
send_raw Msgproto::AccessControlList.new(
|
129
|
+
type: Msgproto::MsgType::ACL,
|
130
|
+
id: SecureRandom.uuid,
|
131
|
+
command: Msgproto::ACLCommand::LIST,
|
132
|
+
)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Sends a message and waits for the response
|
137
|
+
#
|
138
|
+
# @params msg [Msgproto::Message] message object to be sent
|
139
|
+
def send_and_wait_for_response(msg, original)
|
140
|
+
wait_for msg.id, original do
|
141
|
+
send_message msg
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Executes the given block and waits for the message id specified on
|
146
|
+
# the uuid.
|
147
|
+
#
|
148
|
+
# @params uuid [string] unique identifier for a conversation
|
149
|
+
def wait_for(uuid, msg = nil)
|
150
|
+
SelfSDK.logger.info "sending #{uuid}"
|
151
|
+
@mon.synchronize do
|
152
|
+
@messages[uuid] = {
|
153
|
+
waiting_cond: @mon.new_cond,
|
154
|
+
waiting: true,
|
155
|
+
timeout: SelfSDK::Time.now + @timeout,
|
156
|
+
original_message: msg,
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
yield
|
161
|
+
|
162
|
+
SelfSDK.logger.info "waiting for client to respond #{uuid}"
|
163
|
+
@mon.synchronize do
|
164
|
+
@messages[uuid][:waiting_cond].wait_while do
|
165
|
+
@messages[uuid][:waiting]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
SelfSDK.logger.info "response received for #{uuid}"
|
170
|
+
@messages[uuid][:response]
|
171
|
+
ensure
|
172
|
+
@messages.delete(uuid)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Send a message through self network
|
176
|
+
#
|
177
|
+
# @params msg [Msgproto::Message] message object to be sent
|
178
|
+
def send_message(msg)
|
179
|
+
uuid = msg.id
|
180
|
+
@mon.synchronize do
|
181
|
+
@acks[uuid] = {
|
182
|
+
waiting_cond: @mon.new_cond,
|
183
|
+
waiting: true,
|
184
|
+
timeout: SelfSDK::Time.now + @ack_timeout,
|
185
|
+
}
|
186
|
+
end
|
187
|
+
send_raw(msg)
|
188
|
+
SelfSDK.logger.info "waiting for acknowledgement #{uuid}"
|
189
|
+
@mon.synchronize do
|
190
|
+
@acks[uuid][:waiting_cond].wait_while do
|
191
|
+
@acks[uuid][:waiting]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
SelfSDK.logger.info "acknowledged #{uuid}"
|
195
|
+
true
|
196
|
+
ensure
|
197
|
+
@acks.delete(uuid)
|
198
|
+
false
|
199
|
+
end
|
200
|
+
|
201
|
+
def clean_observers
|
202
|
+
live = {}
|
203
|
+
@uuid_observer.clone.each do |id, msg|
|
204
|
+
if msg[:timeout] < SelfSDK::Time.now
|
205
|
+
message = SelfSDK::Messages::Base.new(self)
|
206
|
+
message.status = "errored"
|
207
|
+
|
208
|
+
@uuid_observer[id][:block].call(message)
|
209
|
+
@uuid_observer.delete(id)
|
210
|
+
else
|
211
|
+
live[id] = msg
|
212
|
+
end
|
213
|
+
end
|
214
|
+
@uuid_observer = live
|
215
|
+
end
|
216
|
+
|
217
|
+
# Notify the type observer for the given message
|
218
|
+
def notify_observer(message)
|
219
|
+
if @uuid_observer.include? message.id
|
220
|
+
observer = @uuid_observer[message.id]
|
221
|
+
message.validate!(observer[:original_message]) if observer.include?(:original_message)
|
222
|
+
Thread.new do
|
223
|
+
@uuid_observer[message.id][:block].call(message)
|
224
|
+
@uuid_observer.delete(message.id)
|
225
|
+
end
|
226
|
+
return
|
227
|
+
end
|
228
|
+
|
229
|
+
# Return if there is no observer setup for this kind of message
|
230
|
+
return unless @type_observer.include? message.typ
|
231
|
+
|
232
|
+
Thread.new do
|
233
|
+
@type_observer[message.typ][:block].call(message)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def set_observer(original, options = {}, &block)
|
238
|
+
request_timeout = options.fetch(:timeout, @timeout)
|
239
|
+
@uuid_observer[original.id] = { original_message: original, block: block, timeout: SelfSDK::Time.now + request_timeout }
|
240
|
+
end
|
241
|
+
|
242
|
+
def subscribe(type, &block)
|
243
|
+
type = SelfSDK::message_type(type) if type.is_a? Symbol
|
244
|
+
@type_observer[type] = { block: block }
|
245
|
+
end
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
# Start sthe websocket listener
|
250
|
+
def start
|
251
|
+
SelfSDK.logger.info "starting"
|
252
|
+
@mon.synchronize do
|
253
|
+
@acks["authentication"] = { waiting_cond: @mon.new_cond,
|
254
|
+
waiting: true,
|
255
|
+
timeout: SelfSDK::Time.now + @ack_timeout }
|
256
|
+
end
|
257
|
+
|
258
|
+
Thread.new do
|
259
|
+
EM.run start_connection
|
260
|
+
end
|
261
|
+
|
262
|
+
Thread.new do
|
263
|
+
loop { sleep 10; clean_timeouts }
|
264
|
+
end
|
265
|
+
|
266
|
+
Thread.new do
|
267
|
+
loop { sleep 30; ping }
|
268
|
+
end
|
269
|
+
|
270
|
+
@mon.synchronize do
|
271
|
+
@acks["authentication"][:waiting_cond].wait_while { @acks["authentication"][:waiting] }
|
272
|
+
@acks.delete("authentication")
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Cleans expired messages
|
277
|
+
def clean_timeouts
|
278
|
+
clean_observers
|
279
|
+
clean_timeouts_for(@messages)
|
280
|
+
clean_timeouts_for(@acks)
|
281
|
+
end
|
282
|
+
|
283
|
+
def clean_timeouts_for(list)
|
284
|
+
list.clone.each do |uuid, _msg|
|
285
|
+
next unless list[uuid][:timeout] < SelfSDK::Time.now
|
286
|
+
|
287
|
+
@mon.synchronize do
|
288
|
+
SelfSDK.logger.info "message response timed out #{uuid}"
|
289
|
+
list[uuid][:waiting] = false
|
290
|
+
list[uuid][:waiting_cond].broadcast
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Creates a websocket connection and sets up its callbacks.
|
296
|
+
def start_connection
|
297
|
+
SelfSDK.logger.info "starting listener"
|
298
|
+
@ws = Faye::WebSocket::Client.new(@url)
|
299
|
+
SelfSDK.logger.info "initialized"
|
300
|
+
|
301
|
+
@ws.on :open do |_event|
|
302
|
+
SelfSDK.logger.info "websocket connection established"
|
303
|
+
authenticate
|
304
|
+
end
|
305
|
+
|
306
|
+
@ws.on :message do |event|
|
307
|
+
on_message(event)
|
308
|
+
end
|
309
|
+
|
310
|
+
@ws.on :close do |event|
|
311
|
+
if event.code == ON_DEMAND_CLOSE_CODE
|
312
|
+
puts "client closed connection"
|
313
|
+
else
|
314
|
+
if !@auto_reconnect
|
315
|
+
raise StandardError "websocket connection closed"
|
316
|
+
end
|
317
|
+
if !@reconnection_delay.nil?
|
318
|
+
SelfSDK.logger.info "websocket connection closed (#{event.code}) #{event.reason}"
|
319
|
+
sleep @reconnection_delay
|
320
|
+
SelfSDK.logger.info "reconnecting..."
|
321
|
+
end
|
322
|
+
@reconnection_delay = 3
|
323
|
+
start_connection
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Pings the websocket server to keep the connection alive.
|
329
|
+
def ping
|
330
|
+
# SelfSDK.logger.info "ping"
|
331
|
+
@ws&.ping
|
332
|
+
end
|
333
|
+
|
334
|
+
# Process an event when it arrives through the websocket connection.
|
335
|
+
def on_message(event)
|
336
|
+
input = Msgproto::Message.decode(event.data.pack('c*'))
|
337
|
+
SelfSDK.logger.info " - received #{input.id} (#{input.type})"
|
338
|
+
case input.type
|
339
|
+
when :ERR
|
340
|
+
SelfSDK.logger.info "error #{input.sender} on #{input.id}"
|
341
|
+
mark_as_arrived(input.id)
|
342
|
+
when :ACK
|
343
|
+
SelfSDK.logger.info "#{input.id} acknowledged"
|
344
|
+
mark_as_acknowledged input.id
|
345
|
+
when :ACL
|
346
|
+
SelfSDK.logger.info "ACL received"
|
347
|
+
process_incomming_acl input
|
348
|
+
when :MSG
|
349
|
+
SelfSDK.logger.info "Message #{input.id} received"
|
350
|
+
process_incomming_message input
|
351
|
+
end
|
352
|
+
rescue TypeError
|
353
|
+
SelfSDK.logger.info "invalid array message"
|
354
|
+
end
|
355
|
+
|
356
|
+
def process_incomming_acl(input)
|
357
|
+
list = JSON.parse(input.recipient)
|
358
|
+
|
359
|
+
@messages['acl_list'][:response] = list
|
360
|
+
mark_as_arrived 'acl_list'
|
361
|
+
rescue StandardError => e
|
362
|
+
p "Error processing incoming ACL #{input.to_json}"
|
363
|
+
SelfSDK.logger.info e
|
364
|
+
SelfSDK.logger.info e.backtrace
|
365
|
+
nil
|
366
|
+
end
|
367
|
+
|
368
|
+
def process_incomming_message(input)
|
369
|
+
message = SelfSDK::Messages.parse(input, self)
|
370
|
+
|
371
|
+
if @messages.include? message.id
|
372
|
+
message.validate! @messages[message.id][:original_message]
|
373
|
+
@messages[message.id][:response] = message
|
374
|
+
mark_as_arrived message.id
|
375
|
+
else
|
376
|
+
SelfSDK.logger.info "Received async message #{input.id}"
|
377
|
+
message.validate! @uuid_observer[message.id][:original_message] if @uuid_observer.include? message.id
|
378
|
+
notify_observer(message)
|
379
|
+
end
|
380
|
+
|
381
|
+
@offset = input.offset
|
382
|
+
write_offset(@offset)
|
383
|
+
rescue StandardError => e
|
384
|
+
p "Error processing incoming message #{input.to_json}"
|
385
|
+
SelfSDK.logger.info e
|
386
|
+
p e.backtrace
|
387
|
+
nil
|
388
|
+
end
|
389
|
+
|
390
|
+
# Authenticates current client on the websocket server.
|
391
|
+
def authenticate
|
392
|
+
SelfSDK.logger.info "authenticating"
|
393
|
+
send_raw Msgproto::Auth.new(
|
394
|
+
type: Msgproto::MsgType::AUTH,
|
395
|
+
id: "authentication",
|
396
|
+
token: @jwt.auth_token,
|
397
|
+
device: @device_id,
|
398
|
+
offset: @offset,
|
399
|
+
)
|
400
|
+
end
|
401
|
+
|
402
|
+
def send_raw(msg)
|
403
|
+
@ws.send(msg.to_proto.bytes)
|
404
|
+
end
|
405
|
+
|
406
|
+
# Marks a message as arrived.
|
407
|
+
def mark_as_arrived(id)
|
408
|
+
# Return if no one is waiting for this message
|
409
|
+
return unless @messages.include? id
|
410
|
+
|
411
|
+
@mon.synchronize do
|
412
|
+
@messages[id][:waiting] = false
|
413
|
+
@messages[id][:waiting_cond].broadcast
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# Marks a message as acknowledged by the server.
|
418
|
+
def mark_as_acknowledged(id)
|
419
|
+
return unless @acks.include? id
|
420
|
+
|
421
|
+
@mon.synchronize do
|
422
|
+
@acks[id][:waiting] = false
|
423
|
+
@acks[id][:waiting_cond].broadcast
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def read_offset
|
428
|
+
return 0 unless File.exist? @offset_file
|
429
|
+
|
430
|
+
File.open(@offset_file, 'rb') do |f|
|
431
|
+
return f.read.unpack('q')[0]
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def write_offset(offset)
|
436
|
+
File.open(@offset_file, 'wb') do |f|
|
437
|
+
f.write([offset].pack('q'))
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|