tinydtls 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e326a7a3bf6820d5596c83553eb9d0cd9c2309ffbb00e24d3045e5a2b69e8c5
4
- data.tar.gz: b446f4a18ca71e313bf5ad06d651e89442ac847526ac181b826fa9fac1d1d99c
3
+ metadata.gz: 3b48d5e01a994d57069810a71731fd775130cb0231a3fc3e8d3ac2e7fb574d7a
4
+ data.tar.gz: 2379fc9678ee3ebdac8c6221f78e6e90287693709268bb3fde5792abdf6478bd
5
5
  SHA512:
6
- metadata.gz: 3e1a1a8fe929488102db4cdf3d2d0ef3ec9dec8518fb74d00ed29afb868ea60010d7ee0b5fe64300a61f6b0cd7633293ccca4cdef4bb3122f6e2e70a27118319
7
- data.tar.gz: 5b873f6d31d0ce0ba9cc3f0795bc8a95b078761ceab382c719acb3a19a1de2a3231873b4cdd86bc419db79682ea154532928261b3503d6906bd58e7c810906c1
6
+ metadata.gz: 9b844e51a0fc9c877b2ca94083af2b043c3654f78d7538fa7d5dfc2e0f83af2546a73328c89a820bc232aaa41e0b05418504dfbc7894a2225a403129b8bacff0
7
+ data.tar.gz: '0585c7d6f6aaa33295807097b30567399ac1de499bb2fc16def2b0a57f0defa22836daa1f438ef9caf8e6ae6fc78548fdd6af95e4f29b0de92a4d30f8d961d0e'
@@ -19,11 +19,14 @@ module TinyDTLS
19
19
  @sendfn = sendfn
20
20
  @queue = queue
21
21
  @secconf = secconf
22
+
23
+ @ffi_struct = Wrapper::DTLSContextStruct.new(
24
+ Wrapper::dtls_new_context(FFI::Pointer.new(key)))
22
25
  end
23
26
 
24
- # Create a new instance of this class from a pointer to a `struct
25
- # dtls_context_t`. Such a pointer is, for instance, passed to the
26
- # various tinydtls callback functions.
27
+ # Retrieves an instance of this class from the TinyDTLS::CONTEXT_MAP
28
+ # using a pointer to a `struct dtls_context_t`. Such a pointer is,
29
+ # for instance, passed to the various tinydtls callback functions.
27
30
  #
28
31
  # The `struct dtls_context_t` which the given pointer points to must
29
32
  # have been created by TinyDTLS::UDPSocket#initialize.
@@ -31,5 +34,17 @@ module TinyDTLS
31
34
  obj = Wrapper::DTLSContextStruct.new(ptr)
32
35
  return CONTEXT_MAP[Wrapper::dtls_get_app_data(obj).to_i]
33
36
  end
37
+
38
+ # Returns a key which should be used to store this context in the
39
+ # global TinyDTLS::CONTEXT_MAP.
40
+ def key
41
+ object_id
42
+ end
43
+
44
+ # Returns an FFI::Struct for this object, representing the
45
+ # underlying `dtls_context_t`.
46
+ def to_ffi
47
+ @ffi_struct
48
+ end
34
49
  end
35
50
  end
@@ -4,6 +4,10 @@ module TinyDTLS
4
4
  # function pointer used in the `dtls_handler_t` struct which is used
5
5
  # by tinydtls to retrieve keys and identities.
6
6
  #
7
+ # The API of this class is quite strict and raises lots of exceptions
8
+ # because it is quite annoying to debug errors occuring in the
9
+ # GetPSKInfo callback.
10
+ #
7
11
  # XXX: Currently this function doesn't map IP address to keys/identities.
8
12
  class SecurityConfig
9
13
  # Implementation of the `get_psk_info` function pointer as used by
@@ -44,27 +48,43 @@ module TinyDTLS
44
48
  end
45
49
  end
46
50
 
47
- # Create a new instance of this class. A #default_key and a
48
- # #default_id can be optionally specified. If they are not specified
49
- # the first key/identity added is used as the default value.
50
- def initialize(default_id = nil, default_key = nil)
51
- @default_id = default_id
52
- @default_key = default_key
53
-
51
+ # Creates a new instance of this class. At least one key/identity
52
+ # pair need to be added to the new instance of this class using
53
+ # #add_client otherwise the #default_id and #default_key methods
54
+ # always raise an error causing TinyDTLS handshakes to fail.
55
+ def initialize
54
56
  @identity_map = Hash.new
55
57
  end
56
58
 
57
- # Adds a security configuration for the given identity.
59
+ # Adds a security configuration for the given identity, key must be
60
+ # non-null otherwise a TypeError is raise.
58
61
  def add_client(id, key)
59
- @identity_map[id] = key
62
+ if key.nil?
63
+ raise TypeError.new("Key must be non-nil")
64
+ else
65
+ @identity_map[id] = key
66
+ end
60
67
  end
61
68
 
62
- # Retrieves the key associated with the given identity.
69
+ # Retrieves the key associated with the given identity, nil is
70
+ # returned if no key was specified for the given identity.
63
71
  def get_key(id)
64
- @identity_map[id]
72
+ if @identity_map.has_key? id
73
+ @identity_map[id]
74
+ end
65
75
  end
66
76
 
77
+ # Retrieves the default identity used for establishing new
78
+ # handshakes. If a #default_id hasn't been explicitly set it returns
79
+ # the first identity added using #add_client.
80
+ #
81
+ # At least one identity must have been added to the instance,
82
+ # otherwise this methods raises a TypeError.
67
83
  def default_id
84
+ if @identity_map.empty?
85
+ raise TypeError.new("Cannot retrieve a default identity from an empty store")
86
+ end
87
+
68
88
  if @default_id.nil?
69
89
  @identity_map.to_a.first.first
70
90
  else
@@ -72,12 +92,48 @@ module TinyDTLS
72
92
  end
73
93
  end
74
94
 
95
+ # Changes the default identity, the given identity must already
96
+ # exist in the store.
97
+ def default_id=(id)
98
+ if @identity_map.has_key? id
99
+ @default_id = id
100
+ else
101
+ raise TypeError.new("Default identity must already exist")
102
+ end
103
+ end
104
+
105
+ # Retrieves the default key used when a call to #GetPSKInfo didn't
106
+ # specify a key. If a #default_key hasn't been explicitly set it
107
+ # returns the first key added using #add_client.
108
+ #
109
+ # At least one key must have been added to the instance,
110
+ # otherwise this methods raises a TypeError.
75
111
  def default_key
112
+ if @identity_map.empty?
113
+ raise TypeError.new("Cannot retrieve a default key from an empty store")
114
+ end
115
+
76
116
  if @default_key.nil?
77
117
  @identity_map.to_a.first.last
78
118
  else
79
119
  @default_key
80
120
  end
81
121
  end
122
+
123
+ # Changes the default key, the given key must already exist in the store.
124
+ def default_key=(key)
125
+ if @identity_map.key(key)
126
+ @default_key = key
127
+ else
128
+ raise TypeError.new("Default key must already exist")
129
+ end
130
+ end
131
+
132
+ # Sets the #default_id and #default_key, restrictions mentioned in
133
+ # #default_id= and #default_key= apply.
134
+ def set_defaults(id, key)
135
+ default_id = id
136
+ default_key = key
137
+ end
82
138
  end
83
139
  end
@@ -3,11 +3,14 @@ module TinyDTLS
3
3
  class Session
4
4
  attr_reader :addrinfo
5
5
 
6
- # Creates a new instance of this class from the given Addrinfo.
6
+ # Creates a new instance of this class from a given Addrinfo
7
+ # instance. This functions allocates memory for the underlying
8
+ # `session_t` type which needs to be freed explicitly freed using
9
+ # #close.
7
10
  def initialize(addrinfo)
8
11
  @addrinfo = addrinfo
9
12
  unless @addrinfo.is_a? Addrinfo
10
- raise TypeError
13
+ raise TypeError.new("Expected Addrinfo or FFI::Pointer")
11
14
  end
12
15
 
13
16
  sockaddr = @addrinfo.to_sockaddr
@@ -17,15 +20,13 @@ module TinyDTLS
17
20
  end
18
21
  end
19
22
 
20
- # Creates a new instance of this class from a pointer to a
21
- # `session_t` tinydtls type. Such a pointer is, for instance, passed
22
- # to the various tinydtls callback functions.
23
- def self.from_ptr(ptr)
23
+ # Extracts an Addrinfo instance of a FFI::Pointer to a `session_t`
24
+ # as returned by #to_ptr.
25
+ def self.addr_from_ptr(ptr)
24
26
  lenptr = Wrapper::SocklenPtr.new
25
27
  sockaddr = Wrapper::dtls_session_addr(ptr, lenptr)
26
28
 
27
- addrinfo = Addrinfo.new(sockaddr.read_string(lenptr[:value]))
28
- return Session.new(addrinfo)
29
+ Addrinfo.new(sockaddr.read_string(lenptr[:value]))
29
30
  end
30
31
 
31
32
  # Converts the object into a C pointer to a `session_t` tinydtls
@@ -35,13 +36,18 @@ module TinyDTLS
35
36
  @session
36
37
  end
37
38
 
38
- # Frees all resources associated with the underlying `session_t`
39
- # tinydtls type and reset any existing connections.
40
- def destroy!(ctx)
41
- peer = Wrapper::dtls_get_peer(ctx, @session)
42
- unless peer.null?
43
- Wrapper::dtls_reset_peer(ctx, peer)
39
+ # Frees all resources associated with the underlying `session_t`.
40
+ # Optionally it also resets all peer connections associated with the
41
+ # session (if any). In order to do so a TinyDTLS::Context needs to
42
+ # be passed.
43
+ def close(ctx = nil)
44
+ unless ctx.nil?
45
+ peer = Wrapper::dtls_get_peer(ctx.to_ffi, @session)
46
+ Wrapper::dtls_reset_peer(ctx.to_ffi, peer) unless peer.null?
44
47
  end
48
+
49
+ Wrapper::dtls_free_session(@session)
50
+ @session = nil
45
51
  end
46
52
  end
47
53
  end
@@ -12,16 +12,24 @@ module TinyDTLS
12
12
  # Default timeout for the cleanup thread in seconds.
13
13
  DEFAULT_TIMEOUT = (5 * 60).freeze
14
14
 
15
- attr_accessor :timeout
15
+ # Timeout used by the cleanup thread. If a session hasn't been used
16
+ # within `timeout * 2` seconds it will be freed automatically.
17
+ attr_reader :timeout
16
18
 
17
19
  # Creates a new instance of this class. A tinydtls `context_t`
18
20
  # pointer is required to free sessions in the background thread.
19
- def initialize(ctx, timeout = DEFAULT_TIMEOUT)
21
+ #
22
+ # Memory for sessions created using #[] needs to be explicitly freed
23
+ # by calling #close as soons as this class instance is no longer
24
+ # needed.
25
+ def initialize(context, timeout = DEFAULT_TIMEOUT)
20
26
  @store = {}
21
27
  @mutex = Mutex.new
28
+
22
29
  @timeout = timeout
30
+ @context = context
23
31
 
24
- start_thread(ctx)
32
+ start_thread
25
33
  end
26
34
 
27
35
  # Retrieve a session from the session manager.
@@ -31,21 +39,28 @@ module TinyDTLS
31
39
  end
32
40
 
33
41
  key = addrinfo.getnameinfo
34
- if @store.has_key? key
35
- sess, _ = @store[key]
36
- else
37
- sess = Session.new(addrinfo)
38
- @store[key] = [sess, true]
39
- end
42
+ @mutex.synchronize do
43
+ if @store.has_key? key
44
+ sess, _ = @store[key]
45
+ else
46
+ sess = Session.new(addrinfo)
47
+ @store[key] = [sess, true]
48
+ end
40
49
 
41
- @mutex.synchronize { f.call(sess) }
50
+ f.call(sess)
51
+ end
42
52
  end
43
53
 
44
- # Frees all ressources associated with this class.
45
- def destroy!
46
- @mutex.lock
47
- @store.clear
48
- @thread.kill
54
+ # Kills the background thread. All established sessions are closed
55
+ # as well, see Session#close.
56
+ def close
57
+ @mutex.synchronize do
58
+ @thread.kill
59
+ @store.each_value do |value|
60
+ sess, _ = value
61
+ sess.close(@context)
62
+ end
63
+ end
49
64
  end
50
65
 
51
66
  private
@@ -55,13 +70,9 @@ module TinyDTLS
55
70
  # as described in Modern Operating Systems, p. 212.
56
71
  #
57
72
  # The thread is only created once.
58
- def start_thread(ctx)
73
+ def start_thread
59
74
  @thread ||= Thread.new do
60
- while true
61
- # XXX: How does concurrent access to variables work in ruby?
62
- # as known as: Is this a concurrency problems since the value
63
- # of @timeout might be changed by a different thread since an
64
- # attr_accessor for it is declared.
75
+ loop do
65
76
  sleep @timeout
66
77
 
67
78
  @mutex.lock
@@ -70,7 +81,7 @@ module TinyDTLS
70
81
  if used
71
82
  [sess, !used]
72
83
  else # Not used since we've been here last time → free resources
73
- sess.destroy!(ctx)
84
+ sess.close(@context)
74
85
  nil
75
86
  end
76
87
  end
@@ -8,8 +8,11 @@ module TinyDTLS
8
8
  #
9
9
  # Basic send and receive methods are implemented and should work.
10
10
  class UDPSocket < ::UDPSocket
11
+ # Maximum of times a dtls_send is retried.
12
+ MAX_RETRY = 5.freeze
13
+
11
14
  Write = Proc.new do |ctx, sess, buf, len|
12
- addrinfo = Session.from_ptr(sess).addrinfo
15
+ addrinfo = Session.addr_from_ptr(sess)
13
16
 
14
17
  ctxobj = TinyDTLS::Context.from_ptr(ctx)
15
18
  ctxobj.sendfn.call(buf.read_string(len),
@@ -18,14 +21,15 @@ module TinyDTLS
18
21
  end
19
22
 
20
23
  Read = Proc.new do |ctx, sess, buf, len|
21
- addrinfo = Session.from_ptr(sess).addrinfo
24
+ addrinfo = Session.addr_from_ptr(sess)
22
25
 
23
26
  # We need to perform a reverse lookup here because
24
27
  # the #recvfrom function needs to return the DNS
25
28
  # hostname.
26
29
  sender = Socket.getaddrinfo(addrinfo.ip_address,
27
- addrinfo.ip_port, nil, :DGRAM,
28
- 0, 0, true).first
30
+ addrinfo.ip_port,
31
+ addrinfo.afamily,
32
+ :DGRAM, 0, 0, true).first
29
33
 
30
34
  ctxobj = TinyDTLS::Context.from_ptr(ctx)
31
35
  ctxobj.queue.push([buf.read_string(len), sender])
@@ -46,27 +50,33 @@ module TinyDTLS
46
50
  @sendfn = method(:send).super_method
47
51
  @secconf = SecurityConfig.new
48
52
 
49
- id = object_id
50
- CONTEXT_MAP[id] = TinyDTLS::Context.new(@sendfn, @queue, @secconf)
51
-
52
- cptr = Wrapper::dtls_new_context(FFI::Pointer.new(id))
53
- @ctx = Wrapper::DTLSContextStruct.new(cptr)
53
+ @context = TinyDTLS::Context.new(@sendfn, @queue, @secconf)
54
+ CONTEXT_MAP[@context.key] = @context
54
55
 
55
56
  if timeout.nil?
56
- @sessions = SessionManager.new(@ctx)
57
+ @sessions = SessionManager.new(@context)
57
58
  else
58
- @sessions = SessionManager.new(@ctx, timeout)
59
+ @sessions = SessionManager.new(@context, timeout)
59
60
  end
60
61
 
61
62
  @handler = Wrapper::DTLSHandlerStruct.new
62
63
  @handler[:write] = UDPSocket::Write
63
64
  @handler[:read] = UDPSocket::Read
64
65
  @handler[:get_psk_info] = SecurityConfig::GetPSKInfo
65
- Wrapper::dtls_set_handler(@ctx, @handler)
66
+ Wrapper::dtls_set_handler(@context.to_ffi, @handler)
66
67
  end
67
68
 
68
- def add_client(id, key)
69
+ # Adds a new identity/key pair to the underlying
70
+ # TinyDTLS::SessionManager. By default the first pair added to the
71
+ # store will be used for establishing new handshakes, this behaviour
72
+ # can be changed using the optional default argument.
73
+ def add_client(id, key, default = false)
69
74
  @secconf.add_client(id, key)
75
+
76
+ if default
77
+ @secconf.default_id = id
78
+ @secconf.default_key = key
79
+ end
70
80
  end
71
81
 
72
82
  def bind(host, port)
@@ -77,18 +87,28 @@ module TinyDTLS
77
87
  # TODO: close_{read,write}
78
88
 
79
89
  def close
80
- @sessions.destroy!
81
- @thread.kill unless @thread.nil?
90
+ # This method can't be called twice since we actually free memory
91
+ # allocated by ruby-ffi in this function. Since we can't free it
92
+ # twice we ensure that this function is only called once.
93
+ return unless CONTEXT_MAP.has_key? @context.key
94
+
95
+ @sessions.close
96
+ unless @thread.nil?
97
+ @thread.kill
98
+ while @thread.alive?
99
+ @thread.join
100
+ end
101
+ end
82
102
 
83
- # DTLS free context sends messages to peers so we need to
84
- # call it before we actually close the underlying socket.
85
- Wrapper::dtls_free_context(@ctx)
103
+ # dtls_free_context sends messages to peers so we need to
104
+ # explicitly free the dtls_context_t before closing the socket.
105
+ Wrapper::dtls_free_context(@context.to_ffi)
86
106
  super
87
107
 
88
108
  # Assuming the @thread is already stopped at this point
89
109
  # we can safely access the CONTEXT_MAP without running
90
110
  # into any kind of concurrency problems.
91
- CONTEXT_MAP.delete(object_id)
111
+ CONTEXT_MAP.delete(@context.key)
92
112
  end
93
113
 
94
114
  def connect(host, port)
@@ -149,20 +169,25 @@ module TinyDTLS
149
169
  port, host = Socket.unpack_sockaddr_in(host)
150
170
  end
151
171
 
152
- addr = Addrinfo.getaddrinfo(host, port, nil, :DGRAM).first
172
+ addr = Addrinfo.getaddrinfo(host, port, @family, :DGRAM).first
153
173
 
154
174
  # If a new thread has been started above a new handshake needs to
155
175
  # be performed by it. We need to block here until the handshake
156
176
  # was completed.
157
177
  #
158
- # The current approach is calling `Wrapper::dtls_write` until it
159
- # succeeds which is suboptimal because it doesn't take into
160
- # account that the handshake may fail.
161
- until (res = dtls_send(addr, mesg)) > 0
178
+ # The current approach is calling `Wrapper::dtls_write` up to
179
+ # MAX_RETRY times. If we didn't manage to send our data to the
180
+ # peer after MAX_RETRY times an exception is raised.
181
+ MAX_RETRY.times do
182
+ res = dtls_send(addr, mesg)
183
+ if res > 0
184
+ return res
185
+ end
186
+
162
187
  sleep 1
163
188
  end
164
189
 
165
- return res
190
+ raise Errno::ECONNREFUSED.new("DTLS handshake failed")
166
191
  end
167
192
 
168
193
  private
@@ -180,7 +205,8 @@ module TinyDTLS
180
205
  # of locking the session manager and is thus thread-safe.
181
206
  def dtls_send(addr, mesg)
182
207
  @sessions[addr] do |sess|
183
- res = Wrapper::dtls_write(@ctx, sess.to_ptr, mesg, mesg.bytesize)
208
+ res = Wrapper::dtls_write(@context.to_ffi, sess.to_ptr,
209
+ mesg, mesg.bytesize)
184
210
  res == -1 ? raise(Errno::EIO) : res
185
211
  end
186
212
  end
@@ -191,13 +217,14 @@ module TinyDTLS
191
217
  # The thread is only created once.
192
218
  def start_thread
193
219
  @thread ||= Thread.new do
194
- while true
220
+ loop do
195
221
  data, addr = method(:recvfrom).super_method
196
222
  .call(Wrapper::DTLS_MAX_BUF)
197
223
  addrinfo = to_addrinfo(*addr)
198
224
 
199
225
  @sessions[addrinfo] do |sess|
200
- Wrapper::dtls_handle_message(@ctx, sess.to_ptr, data, data.bytesize)
226
+ Wrapper::dtls_handle_message(@context.to_ffi, sess.to_ptr,
227
+ data, data.bytesize)
201
228
  end
202
229
  end
203
230
  end
@@ -112,6 +112,7 @@ module TinyDTLS
112
112
  # sockaddr_in{,6}`.
113
113
  attach_function :dtls_new_session,
114
114
  [:pointer, :socklen_t], :pointer
115
+ attach_function :dtls_free_session, [:pointer], :void
115
116
  attach_function :dtls_session_addr, [:pointer, SocklenPtr], :pointer
116
117
 
117
118
  def self.dtls_get_app_data(ctx)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tinydtls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sören Tempel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-14 00:00:00.000000000 Z
11
+ date: 2018-08-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -72,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
72
  version: '0'
73
73
  requirements: []
74
74
  rubyforge_project:
75
- rubygems_version: 2.7.3
75
+ rubygems_version: 2.7.6
76
76
  signing_key:
77
77
  specification_version: 4
78
78
  summary: It wraps the tinydtls library