daytona 0.199.0 → 0.200.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: 10a8c1d98ef86a3f5cb94cef3bb6df610bcfbb5abf59a585eaa76c3b5c527fdd
4
- data.tar.gz: d6159db7b2febce21f6670c7340a792b10cecedac1b20178bf506a44926d7467
3
+ metadata.gz: de70d9e56ce97468d9ef3bcf8c7dbe0b70f74ee7e64a7ac1d0e8642857a3c257
4
+ data.tar.gz: 3cec447b03a65f42bcd84c49e719cd8771a1404f496384afd5e797b429e47fb1
5
5
  SHA512:
6
- metadata.gz: 434cb81dc3376883e3bdce1456f5d1bcad59c51a6cf0e0390e629fdb61f80fbaccf7475fe210e872e8214b3dc6a399da1e9b9b865e950c855d370bd6cf30fd6e
7
- data.tar.gz: 7ad98fd8a62ba96305475cb3742376cd7c48d729d3bd2a7b49f0e0a8075ff188671d03285497cf8abfc429c13a7e5cd98b55e29147e88c8f1cc982fe8d4815ce
6
+ metadata.gz: 91c94980be6003996bbbf4b447b170e9165cbc3a47e78c47f91886f28accd5f7b083b04903fc59dede9bbe56ccdcaf9957da73e07202e762ca928203274b2c8b
7
+ data.tar.gz: db39e610c03a011d4c2468475393ca7b7bd17cc1087dbb0edd46c9fc6959da18e0ca61d861485ca0b27faa8ccb4061f631ec44c97d90d8e6bea7db75317db4e3
@@ -5,6 +5,7 @@
5
5
 
6
6
  require 'timeout'
7
7
 
8
+ require 'openssl'
8
9
  require 'websocket-client-simple'
9
10
  require 'json'
10
11
  require 'uri'
@@ -52,6 +53,8 @@ module Daytona
52
53
  @connected = false
53
54
  @mutex = Mutex.new
54
55
  @write_mutex = Mutex.new
56
+ @verify_mutex = Mutex.new
57
+ @tls_verified = nil
55
58
  @health_thread = nil
56
59
  @ping_interval = 25
57
60
  @ping_timeout = 20
@@ -67,13 +70,24 @@ module Daytona
67
70
  ws_url = build_ws_url
68
71
  connected_queue = Queue.new
69
72
 
73
+ # Reset per-connection TLS state so a reconnect re-verifies its new socket
74
+ # instead of reusing a cached result from the previous connection.
75
+ @verify_mutex.synchronize { @tls_verified = nil }
76
+
70
77
  # Capture self because websocket-client-simple uses instance_exec for callbacks
71
78
  client = self
72
79
 
73
80
  # Register callbacks inside the connect block so they are installed before the
74
81
  # socket dials — otherwise the Engine.IO handshake can arrive before :message
75
82
  # is bound, stalling the connection into a polling fallback/timeout.
76
- @ws = WebSocket::Client::Simple.connect(ws_url) do |ws|
83
+ #
84
+ # verify_mode is passed explicitly: websocket-client-simple builds its own
85
+ # SSLContext and defaults to no certificate verification (VERIFY_NONE).
86
+ @ws = WebSocket::Client::Simple.connect(ws_url, verify_mode: OpenSSL::SSL::VERIFY_PEER) do |ws|
87
+ # Capture the client before its reader thread starts so #verify_tls_peer!
88
+ # can reach the SSL socket even when a frame arrives before .connect returns.
89
+ @ws = ws
90
+
77
91
  ws.on :message do |msg|
78
92
  client.send(:handle_raw_message, msg.data.to_s, connected_queue)
79
93
  end
@@ -104,15 +118,26 @@ module Daytona
104
118
  end
105
119
  end
106
120
 
107
- # Wait for connection with timeout
108
121
  result = nil
109
122
  begin
110
- Timeout.timeout(@connect_timeout) { result = connected_queue.pop }
123
+ Timeout.timeout(@connect_timeout) do
124
+ ensure_tls_verified!(connected_queue)
125
+ result = connected_queue.pop
126
+ if result == :open
127
+ send_connect_auth
128
+ result = connected_queue.pop
129
+ end
130
+ end
111
131
  rescue Timeout::Error
112
132
  close
113
133
  raise "WebSocket connection timed out after #{@connect_timeout}s"
114
134
  end
115
135
 
136
+ if result.is_a?(OpenSSL::SSL::SSLError)
137
+ close
138
+ raise result
139
+ end
140
+
116
141
  if result != :connected
117
142
  close
118
143
  raise "WebSocket connection failed: #{result}"
@@ -141,6 +166,46 @@ module Daytona
141
166
 
142
167
  private
143
168
 
169
+ # Verify the TLS peer's hostname exactly once, before any inbound frame is
170
+ # processed and before the auth frame is sent. websocket-client-simple
171
+ # verifies the certificate chain (VERIFY_PEER) during the handshake but never
172
+ # checks that the certificate matches the host, and its reader thread starts
173
+ # before #connect regains control — so a certificate that is valid for the
174
+ # wrong host could otherwise inject Socket.IO events (and receive the CONNECT
175
+ # auth frame) before verification runs. Gating both the reader path and the
176
+ # connect path guarantees no frame reaches the dispatcher, and no credentials
177
+ # are sent, until the peer is verified.
178
+ # @return [Boolean] true when verified (or plaintext ws://, no TLS peer to
179
+ # check); false on failure, after signalling the connecting thread.
180
+ def ensure_tls_verified!(connected_queue)
181
+ @verify_mutex.synchronize do
182
+ return @tls_verified unless @tls_verified.nil?
183
+
184
+ begin
185
+ verify_tls_peer!
186
+ @tls_verified = true
187
+ rescue OpenSSL::SSL::SSLError => e
188
+ @tls_verified = false
189
+ @mutex.synchronize { @connected = false }
190
+ connected_queue&.push(e)
191
+ end
192
+
193
+ @tls_verified
194
+ end
195
+ end
196
+
197
+ def verify_tls_peer!
198
+ socket = @ws&.instance_variable_get(:@socket)
199
+ return unless socket.is_a?(OpenSSL::SSL::SSLSocket)
200
+
201
+ socket.post_connection_check(URI.parse(@api_url).host)
202
+ end
203
+
204
+ def send_connect_auth
205
+ auth = JSON.generate({ token: @token })
206
+ send_raw("#{EIO_MESSAGE}#{SIO_CONNECT}#{auth}")
207
+ end
208
+
144
209
  def build_ws_url
145
210
  parsed = URI.parse(@api_url)
146
211
  ws_scheme = parsed.scheme == 'https' ? 'wss' : 'ws'
@@ -160,6 +225,11 @@ module Daytona
160
225
  def handle_raw_message(raw, connected_queue)
161
226
  return if raw.nil? || raw.empty?
162
227
 
228
+ # Drop every frame from a peer whose hostname is not yet verified. The
229
+ # reader thread can run before #connect verifies, so this is the gate that
230
+ # stops a wrong-host peer from injecting events or eliciting the auth frame.
231
+ return unless ensure_tls_verified!(connected_queue)
232
+
163
233
  # Track all server activity for health monitoring.
164
234
  # If the server stops sending ANY data (pings, events, etc.)
165
235
  # the health monitor will detect the dead connection.
@@ -175,9 +245,10 @@ module Daytona
175
245
  rescue JSON::ParserError
176
246
  # Use default ping interval
177
247
  end
178
- # Send Socket.IO CONNECT with auth
179
- auth = JSON.generate({ token: @token })
180
- send_raw("#{EIO_MESSAGE}#{SIO_CONNECT}#{auth}")
248
+ # Signal the connecting thread instead of sending the CONNECT auth frame
249
+ # here: the token must not leave this process until the connecting thread
250
+ # has completed TLS peer verification (see #connect).
251
+ connected_queue&.push(:open)
181
252
 
182
253
  when EIO_PING
183
254
  # Server heartbeat — respond immediately with PONG
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Daytona
7
7
  module Sdk
8
- VERSION = '0.199.0'
8
+ VERSION = '0.200.0'
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: daytona
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.199.0
4
+ version: 0.200.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daytona Platforms Inc.
@@ -85,42 +85,42 @@ dependencies:
85
85
  requirements:
86
86
  - - '='
87
87
  - !ruby/object:Gem::Version
88
- version: 0.199.0
88
+ version: 0.200.0
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - '='
94
94
  - !ruby/object:Gem::Version
95
- version: 0.199.0
95
+ version: 0.200.0
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: daytona_api_client
98
98
  requirement: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 0.199.0
102
+ version: 0.200.0
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - '='
108
108
  - !ruby/object:Gem::Version
109
- version: 0.199.0
109
+ version: 0.200.0
110
110
  - !ruby/object:Gem::Dependency
111
111
  name: daytona_toolbox_api_client
112
112
  requirement: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - '='
115
115
  - !ruby/object:Gem::Version
116
- version: 0.199.0
116
+ version: 0.200.0
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - '='
122
122
  - !ruby/object:Gem::Version
123
- version: 0.199.0
123
+ version: 0.200.0
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: dotenv
126
126
  requirement: !ruby/object:Gem::Requirement