tinydtls 0.1.0 → 0.2.0

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