15
15
KEEP_ALIVE_TIMEOUT ,
16
16
KEY_LENGTH ,
17
17
AddressType ,
18
+ ClientState ,
18
19
OverflowSentinel ,
19
20
available_slot_range ,
20
21
is_initiator_id ,
@@ -94,6 +95,9 @@ def set_initiator(self, initiator):
94
95
Arguments:
95
96
- `initiator`: A :class:`PathClient` instance.
96
97
98
+ Raises :exc:`ValueError` in case of a state violation on the
99
+ :class:`PathClient`.
100
+
97
101
Return the previously set initiator or `None`.
98
102
"""
99
103
previous_initiator = self ._slots .get (AddressType .initiator )
@@ -102,8 +106,7 @@ def set_initiator(self, initiator):
102
106
# Update initiator's log name
103
107
initiator .update_log_name (AddressType .initiator )
104
108
# Authenticated, assign id
105
- initiator .authenticated = True
106
- initiator .id = AddressType .initiator
109
+ initiator .authenticate (AddressType .initiator )
107
110
# Return previous initiator
108
111
return previous_initiator
109
112
@@ -141,7 +144,10 @@ def add_responder(self, responder):
141
144
Arguments:
142
145
- `client`: A :class:`PathClient` instance.
143
146
144
- Raises :exc:`SlotsFullError` if no free slot exists on the path.
147
+ Raises:
148
+ - :exc:`SlotsFullError` if no free slot exists on the path.
149
+ - :exc:`ValueError` in case of a state violation on the
150
+ :class:`PathClient`.
145
151
146
152
Return the assigned slot identifier.
147
153
"""
@@ -152,8 +158,7 @@ def add_responder(self, responder):
152
158
# Update responder's log name
153
159
responder .update_log_name (id_ )
154
160
# Authenticated, set and return assigned slot id
155
- responder .authenticated = True
156
- responder .id = id_
161
+ responder .authenticate (id_ )
157
162
return id_
158
163
raise SlotsFullError ('No free slots on path' )
159
164
@@ -172,8 +177,8 @@ def remove_client(self, client):
172
177
Raises :exc:`KeyError` in case the client provided an
173
178
invalid slot identifier.
174
179
"""
175
- if not client .authenticated :
176
- # Client has not been authenticated. Nothing to do.
180
+ if client .state == ClientState . restricted :
181
+ # Client has never been authenticated. Nothing to do.
177
182
return
178
183
id_ = client .id
179
184
@@ -245,6 +250,7 @@ def _ensure_future_or_none(coroutine_or_task, loop):
245
250
class PathClient :
246
251
__slots__ = (
247
252
'_loop' ,
253
+ '_state' ,
248
254
'_connection' ,
249
255
'_connection_closed_future' ,
250
256
'_client_key' ,
@@ -262,7 +268,6 @@ class PathClient:
262
268
'_keep_alive_interval' ,
263
269
'log' ,
264
270
'type' ,
265
- 'authenticated' ,
266
271
'keep_alive_timeout' ,
267
272
'keep_alive_pings' ,
268
273
'tasks' ,
@@ -275,6 +280,7 @@ def __init__(
275
280
server_session_key = None , loop = None
276
281
):
277
282
self ._loop = asyncio .get_event_loop () if loop is None else loop
283
+ self ._state = ClientState .restricted
278
284
self ._connection = connection
279
285
connection_closed_future = asyncio .Future (loop = self ._loop )
280
286
self ._connection_closed_future = connection_closed_future
@@ -291,7 +297,6 @@ def __init__(
291
297
self ._keep_alive_interval = KEEP_ALIVE_INTERVAL_DEFAULT
292
298
self .log = util .get_logger ('path.{}.client.{:x}' .format (path_number , id (self )))
293
299
self .type = None
294
- self .authenticated = False
295
300
self .keep_alive_timeout = KEEP_ALIVE_TIMEOUT
296
301
self .keep_alive_pings = 0
297
302
self .tasks = None
@@ -312,6 +317,26 @@ def __str__(self):
312
317
return 'PathClient(role=0x{:02x}, id={}, at={})' .format (
313
318
type_ , self ._id , hex (id (self )))
314
319
320
+ @property
321
+ def state (self ):
322
+ """
323
+ Return the current :class:`ClientState` of the client.
324
+ """
325
+ return self ._state
326
+
327
+ @state .setter
328
+ def state (self , state ):
329
+ """
330
+ Update the :class:`ClientState` of the client.
331
+
332
+ Raises :exc:`ValueError` in case the state is not following
333
+ the strict state order as defined by :class`ClientState`.
334
+ """
335
+ if state != self .state .next :
336
+ raise ValueError ('State {} cannot be updated to {}' .format (self .state , state ))
337
+ self .log .debug ('State {} -> {}' , self ._state .name , state .name )
338
+ self ._state = state
339
+
315
340
@property
316
341
def connection_closed_future (self ):
317
342
"""
@@ -328,14 +353,6 @@ def id(self):
328
353
"""
329
354
return self ._id
330
355
331
- @id .setter
332
- def id (self , id_ ):
333
- """
334
- Assign the id. Only :class:`Path` may set the id!
335
- """
336
- self ._id = id_
337
- self .log .debug ('Assigned id: {}' , id_ )
338
-
339
356
@property
340
357
def keep_alive_interval (self ):
341
358
"""
@@ -495,6 +512,19 @@ def set_client_key(self, public_key):
495
512
self ._box = libnacl .public .Box (self .server_key , public_key )
496
513
self .log .debug ('Client key updated' )
497
514
515
+ def authenticate (self , id_ ):
516
+ """
517
+ Authenticate the client and assign it an id.
518
+
519
+ .. important:: Only :class:`Path` may call this!
520
+
521
+ Raises :exc:`ValueError` in case the previous state was not
522
+ :attr:`ClientState.restricted`.
523
+ """
524
+ self .state = ClientState .authenticated
525
+ self ._id = id_
526
+ self .log .debug ('Assigned id: {}' , id_ )
527
+
498
528
def update_log_name (self , slot_id ):
499
529
"""
500
530
Update the logger's name by the assigned slot identifier.
@@ -563,7 +593,7 @@ def p2p_allowed(self, destination_type):
563
593
Return `True` if :class:`RawMessage` instances are allowed and
564
594
can be sent to the requested :class:`AddressType`.
565
595
"""
566
- return self .authenticated and self .type != destination_type
596
+ return self .state == ClientState . authenticated and self .type != destination_type
567
597
568
598
@asyncio .coroutine
569
599
def enqueue_task (self , coroutine_or_task , ignore_closed = False ):
@@ -732,6 +762,11 @@ def receive(self):
732
762
"""
733
763
Disconnected
734
764
"""
765
+ # Safeguard
766
+ # Note: This should never happen since the receive queue will
767
+ # be stopped when a client is being dropped.
768
+ assert self .state < ClientState .dropped
769
+
735
770
# Receive data
736
771
try :
737
772
data = yield from self ._connection .recv ()
@@ -825,7 +860,8 @@ def drop(self, code):
825
860
self .log .debug ('Cancelling all running tasks but the task loop' )
826
861
self .tasks .cancel_all_but_task_loop ()
827
862
828
- # Dropped
863
+ # Mark as dropped
864
+ self .state = ClientState .dropped
829
865
self .log .debug ('Client dropped, close code: {}' , code )
830
866
return enqueue_task
831
867
0 commit comments