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