1818using System . IO ;
1919using System . Net . Sockets ;
2020using System . Text ;
21+ using System . Security . Cryptography ;
2122using System . Text . Json ;
2223using System . Threading ;
2324using System . Threading . Tasks ;
@@ -116,7 +117,7 @@ public async Task<bool> ConnectAsync()
116117 }
117118
118119 /// <summary>
119- /// Authenticates with the DataBento gateway
120+ /// Authenticates with the DataBento gateway using CRAM
120121 /// </summary>
121122 private async Task < bool > AuthenticateAsync ( )
122123 {
@@ -125,35 +126,104 @@ private async Task<bool> AuthenticateAsync()
125126
126127 try
127128 {
128- // DataBento authentication message format
129- var authMessage = new
129+ // Read greeting and challenge from gateway
130+ var responseBuffer = new byte [ 1024 ] ;
131+ var messageBuffer = new StringBuilder ( ) ;
132+
133+ // Read until we have both the version and cram messages
134+ string ? version = null ;
135+ string ? cram = null ;
136+
137+ while ( version == null || cram == null )
130138 {
131- msg_type = "authenticate" ,
132- api_key = _apiKey ,
133- version = "1.0"
134- } ;
135-
136- var authJson = JsonSerializer . Serialize ( authMessage ) ;
137- var authBytes = Encoding . UTF8 . GetBytes ( authJson + "\n " ) ;
139+ var authenticationBytesRead = await _stream . ReadAsync ( responseBuffer , 0 , responseBuffer . Length ) ;
140+ if ( authenticationBytesRead == 0 )
141+ {
142+ Log . Error ( "DatabentoRawClient.AuthenticateAsync(): Connection closed during authentication" ) ;
143+ return false ;
144+ }
145+
146+ var receivedData = Encoding . UTF8 . GetString ( responseBuffer , 0 , authenticationBytesRead ) ;
147+ messageBuffer . Append ( receivedData ) ;
148+
149+ var messages = messageBuffer . ToString ( ) . Split ( '\n ' , StringSplitOptions . RemoveEmptyEntries ) ;
150+
151+ foreach ( var msg in messages )
152+ {
153+ if ( msg . StartsWith ( "lsg_version=" ) )
154+ {
155+ version = msg . Substring ( "lsg_version=" . Length ) ;
156+ Log . Debug ( $ "DatabentoRawClient.AuthenticateAsync(): Gateway version: { version } ") ;
157+ }
158+ else if ( msg . StartsWith ( "cram=" ) )
159+ {
160+ cram = msg . Substring ( "cram=" . Length ) ;
161+ Log . Debug ( $ "DatabentoRawClient.AuthenticateAsync(): Received CRAM challenge") ;
162+ }
163+ }
164+ }
138165
166+ if ( string . IsNullOrEmpty ( cram ) )
167+ {
168+ Log . Error ( "DatabentoRawClient.AuthenticateAsync(): No CRAM challenge received" ) ;
169+ return false ;
170+ }
171+
172+ // Generate authentication response
173+ // Concatenate cram and API key: $cram|$key
174+ var cramAndKey = $ "{ cram } |{ _apiKey } ";
175+
176+ // Hash with SHA-256
177+ string authHash ;
178+ using ( var sha256 = SHA256 . Create ( ) )
179+ {
180+ var hashBytes = sha256 . ComputeHash ( Encoding . UTF8 . GetBytes ( cramAndKey ) ) ;
181+ authHash = BitConverter . ToString ( hashBytes ) . Replace ( "-" , "" ) . ToLowerInvariant ( ) ;
182+ }
183+
184+ // Get last 5 characters of API key (bucket_id)
185+ var bucketId = _apiKey . Substring ( _apiKey . Length - 5 ) ;
186+
187+ // Create auth response: $hash-$bucket_id
188+ var authResponse = $ "{ authHash } -{ bucketId } ";
189+
190+ // Send authentication message
191+ // Format: auth=$authResponse|dataset=GLBX.MDP3|encoding=dbn|ts_out=0\n
192+ var authMessage = $ "auth={ authResponse } |dataset=GLBX.MDP3|encoding=dbn|ts_out=0\n ";
193+ var authBytes = Encoding . UTF8 . GetBytes ( authMessage ) ;
194+
139195 await _stream . WriteAsync ( authBytes , 0 , authBytes . Length ) ;
140196 await _stream . FlushAsync ( ) ;
141-
197+
198+ Log . Debug ( "DatabentoRawClient.AuthenticateAsync(): Sent authentication message" ) ;
199+
142200 // Read authentication response
143- var responseBuffer = new byte [ 1024 ] ;
201+ messageBuffer . Clear ( ) ;
144202 var bytesRead = await _stream . ReadAsync ( responseBuffer , 0 , responseBuffer . Length ) ;
145203 var response = Encoding . UTF8 . GetString ( responseBuffer , 0 , bytesRead ) ;
146-
204+
147205 Log . Debug ( $ "DatabentoRawClient.AuthenticateAsync(): Auth response: { response } ") ;
148-
149- // Parse response to check if authentication was successful
150- var responseDoc = JsonDocument . Parse ( response . Trim ( ) ) ;
151- if ( responseDoc . RootElement . TryGetProperty ( "success" , out var successElement ) )
206+
207+ // Check for success response: success=1|session_id=...
208+ if ( response . Contains ( "success=1" ) )
152209 {
153- return successElement . GetBoolean ( ) ;
210+ // Extract session ID if needed
211+ if ( response . Contains ( "session_id=" ) )
212+ {
213+ var sessionIdStart = response . IndexOf ( "session_id=" ) + "session_id=" . Length ;
214+ var sessionIdEnd = response . IndexOf ( '|' , sessionIdStart ) ;
215+ if ( sessionIdEnd == - 1 ) sessionIdEnd = response . IndexOf ( '\n ' , sessionIdStart ) ;
216+ if ( sessionIdEnd > sessionIdStart )
217+ {
218+ var sessionId = response . Substring ( sessionIdStart , sessionIdEnd - sessionIdStart ) ;
219+ Log . Debug ( $ "DatabentoRawClient.AuthenticateAsync(): Session ID: { sessionId } ") ;
220+ }
221+ }
222+ return true ;
154223 }
155-
156- return response . Contains ( "success" ) || response . Contains ( "authenticated" ) ;
224+
225+ Log . Trace ( $ "DatabentoRawClient.AuthenticateAsync(): Authentication failed: { response } ") ;
226+ return false ;
157227 }
158228 catch ( Exception ex )
159229 {
0 commit comments