11use std:: io:: Cursor as IOCursor ;
2+ use std:: pin:: Pin ;
3+ use std:: sync:: Arc ;
4+ use std:: time:: Duration ;
25
3- use btleplug:: api:: { Peripheral as _, WriteType } ;
6+ use btleplug:: api:: { Peripheral as _, ValueNotification , WriteType } ;
47use btleplug:: platform:: Peripheral ;
58use byteorder:: { BigEndian , ReadBytesExt } ;
9+ use futures:: stream:: { Stream , StreamExt } ;
10+ use tokio:: sync:: Mutex ;
11+ use tokio:: time:: timeout;
612use tracing:: { debug, info, instrument, trace, warn} ;
713
814use super :: device:: FidoEndpoints ;
15+ use super :: gatt:: write_type_for;
916use super :: Error ;
1017use crate :: fido:: FidoRevision ;
1118use crate :: transport:: ble:: framing:: {
1219 BleCommand , BleFrame as Frame , BleFrameParser , BleFrameParserResult ,
1320} ;
1421
15- #[ derive( Debug , Clone ) ]
22+ type NotificationStream = Pin < Box < dyn Stream < Item = ValueNotification > + Send > > ;
23+
24+ #[ derive( Clone ) ]
1625pub struct Connection {
1726 pub peripheral : Peripheral ,
1827 pub services : FidoEndpoints ,
28+ /// `fidoStatus` is Notify-only (CTAP 2.2 §11.4); we consume notifications
29+ /// rather than issue GATT Read.
30+ notifications : Arc < Mutex < NotificationStream > > ,
31+ }
32+
33+ impl std:: fmt:: Debug for Connection {
34+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
35+ f. debug_struct ( "Connection" )
36+ . field ( "peripheral" , & self . peripheral )
37+ . field ( "services" , & self . services )
38+ . finish_non_exhaustive ( )
39+ }
1940}
2041
2142impl Connection {
@@ -24,9 +45,26 @@ impl Connection {
2445 services : & FidoEndpoints ,
2546 revision : & FidoRevision ,
2647 ) -> Result < Self , Error > {
48+ // Subscribe before opening the stream so early frames aren't dropped.
49+ peripheral
50+ . subscribe ( & services. status )
51+ . await
52+ . or ( Err ( Error :: OperationFailed ) ) ?;
53+
54+ let status_uuid = services. status . uuid ;
55+ let raw_stream = peripheral
56+ . notifications ( )
57+ . await
58+ . or ( Err ( Error :: OperationFailed ) ) ?;
59+ let notifications: NotificationStream = Box :: pin ( raw_stream. filter ( move |n| {
60+ let matches = n. uuid == status_uuid;
61+ async move { matches }
62+ } ) ) ;
63+
2764 let connection = Self {
2865 peripheral : peripheral. to_owned ( ) ,
2966 services : services. clone ( ) ,
67+ notifications : Arc :: new ( Mutex :: new ( notifications) ) ,
3068 } ;
3169 connection. select_fido_revision ( revision) . await ?;
3270 Ok ( connection)
@@ -60,16 +98,14 @@ impl Connection {
6098 . fragments ( max_fragment_size)
6199 . or ( Err ( Error :: InvalidFraming ) ) ?;
62100
101+ let write_type = write_type_for ( & self . services . control_point ) ;
102+
63103 for ( i, fragment) in fragments. iter ( ) . enumerate ( ) {
64104 debug ! ( { fragment = i, len = fragment. len( ) } , "Sending fragment" ) ;
65105 trace ! ( ?fragment) ;
66106
67107 self . peripheral
68- . write (
69- & self . services . control_point ,
70- fragment,
71- WriteType :: WithoutResponse ,
72- )
108+ . write ( & self . services . control_point , fragment, write_type)
73109 . await
74110 . or ( Err ( Error :: OperationFailed ) ) ?;
75111 }
@@ -79,25 +115,63 @@ impl Connection {
79115
80116 pub ( crate ) async fn select_fido_revision ( & self , revision : & FidoRevision ) -> Result < ( ) , Error > {
81117 let ack: u8 = * revision as u8 ;
118+ let write_type = write_type_for ( & self . services . service_revision_bitfield ) ;
82119 self . peripheral
83- . write (
84- & self . services . service_revision_bitfield ,
85- & [ ack] ,
86- WriteType :: WithoutResponse ,
87- )
120+ . write ( & self . services . service_revision_bitfield , & [ ack] , write_type)
88121 . await
89122 . or ( Err ( Error :: OperationFailed ) ) ?;
90123
91124 info ! ( ?revision, "Successfully selected FIDO revision" ) ;
92125 Ok ( ( ) )
93126 }
94127
128+ /// Sends a best-effort Cancel on `fidoControlPoint` using
129+ /// `WriteType::WithoutResponse` so cancellation never blocks.
130+ async fn send_cancel ( & self ) -> Result < ( ) , Error > {
131+ let cancel_frame = Frame :: new ( BleCommand :: Cancel , & [ ] ) ;
132+ let max_fragment_size = self . control_point_length ( ) . await . unwrap_or ( 20 ) ;
133+ let fragments = cancel_frame
134+ . fragments ( max_fragment_size)
135+ . or ( Err ( Error :: InvalidFraming ) ) ?;
136+ for fragment in fragments {
137+ self . peripheral
138+ . write (
139+ & self . services . control_point ,
140+ & fragment,
141+ WriteType :: WithoutResponse ,
142+ )
143+ . await
144+ . or ( Err ( Error :: OperationFailed ) ) ?;
145+ }
146+ Ok ( ( ) )
147+ }
148+
95149 #[ instrument( skip_all) ]
96- pub async fn frame_recv ( & self ) -> Result < Frame , Error > {
150+ pub async fn frame_recv ( & self , op_timeout : Duration ) -> Result < Frame , Error > {
97151 let mut parser = BleFrameParser :: new ( ) ;
152+ let mut stream = self . notifications . lock ( ) . await ;
98153
99154 loop {
100- let fragment = self . receive_fragment ( ) . await ?;
155+ let fragment = match timeout ( op_timeout, stream. next ( ) ) . await {
156+ Ok ( Some ( notification) ) => notification. value ,
157+ Ok ( None ) => {
158+ warn ! ( "Notification stream ended unexpectedly" ) ;
159+ return Err ( Error :: ConnectionFailed ) ;
160+ }
161+ Err ( _) => {
162+ warn ! (
163+ ?op_timeout,
164+ "Timed out waiting for fidoStatus notification; sending Cancel"
165+ ) ;
166+ // Drop the lock so a late notification doesn't deadlock the cancel.
167+ drop ( stream) ;
168+ if let Err ( e) = self . send_cancel ( ) . await {
169+ warn ! ( ?e, "Failed to send Cancel after timeout" ) ;
170+ }
171+ return Err ( Error :: Timeout ) ;
172+ }
173+ } ;
174+
101175 debug ! ( "Received fragment" ) ;
102176 trace ! ( ?fragment) ;
103177
@@ -133,13 +207,7 @@ impl Connection {
133207 }
134208 }
135209
136- async fn receive_fragment ( & self ) -> Result < Vec < u8 > , Error > {
137- self . peripheral
138- . read ( & self . services . status )
139- . await
140- . or ( Err ( Error :: OperationFailed ) )
141- }
142-
210+ /// Enables notifications on `fidoStatus`. Idempotent.
143211 pub async fn subscribe ( & self ) -> Result < ( ) , Error > {
144212 self . peripheral
145213 . subscribe ( & self . services . status )
0 commit comments