@@ -7,7 +7,6 @@ import com.coder.toolbox.sdk.convertors.LoggingConverterFactory
77import com.coder.toolbox.sdk.convertors.OSConverter
88import com.coder.toolbox.sdk.convertors.UUIDConverter
99import com.coder.toolbox.sdk.ex.APIResponseException
10- import com.coder.toolbox.sdk.interceptors.CertificateRefreshInterceptor
1110import com.coder.toolbox.sdk.interceptors.Interceptors
1211import com.coder.toolbox.sdk.v2.CoderV2RestFacade
1312import com.coder.toolbox.sdk.v2.models.ApiErrorResponse
@@ -23,7 +22,10 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceResource
2322import com.coder.toolbox.sdk.v2.models.WorkspaceTransition
2423import com.coder.toolbox.util.ReloadableTlsContext
2524import com.squareup.moshi.Moshi
25+ import kotlinx.coroutines.Dispatchers
26+ import kotlinx.coroutines.withContext
2627import okhttp3.OkHttpClient
28+ import org.zeroturnaround.exec.ProcessExecutor
2729import retrofit2.Response
2830import retrofit2.Retrofit
2931import retrofit2.converter.moshi.MoshiConverterFactory
@@ -72,8 +74,6 @@ open class CoderRestClient(
7274 throw IllegalStateException (" Token is required for $url deployment" )
7375 }
7476 add(Interceptors .tokenAuth(token))
75- } else if (context.settingsStore.requiresMTlsAuth && context.settingsStore.tls.certRefreshCommand?.isNotBlank() == true ) {
76- add(CertificateRefreshInterceptor (context, tlsContext))
7777 }
7878 add((Interceptors .userAgent(pluginVersion)))
7979 add(Interceptors .externalHeaders(context, url))
@@ -114,7 +114,7 @@ open class CoderRestClient(
114114 * @throws [APIResponseException].
115115 */
116116 internal suspend fun me (): User {
117- val userResponse = retroRestClient.me()
117+ val userResponse = callWithRetry { retroRestClient.me() }
118118 if (! userResponse.isSuccessful) {
119119 throw APIResponseException (
120120 " initializeSession" ,
@@ -133,7 +133,7 @@ open class CoderRestClient(
133133 * Retrieves the visual dashboard configuration.
134134 */
135135 internal suspend fun appearance (): Appearance {
136- val appearanceResponse = retroRestClient.appearance()
136+ val appearanceResponse = callWithRetry { retroRestClient.appearance() }
137137 if (! appearanceResponse.isSuccessful) {
138138 throw APIResponseException (
139139 " initializeSession" ,
@@ -153,7 +153,7 @@ open class CoderRestClient(
153153 * @throws [APIResponseException].
154154 */
155155 suspend fun workspaces (): List <Workspace > {
156- val workspacesResponse = retroRestClient.workspaces(" owner:me" )
156+ val workspacesResponse = callWithRetry { retroRestClient.workspaces(" owner:me" ) }
157157 if (! workspacesResponse.isSuccessful) {
158158 throw APIResponseException (
159159 " retrieve workspaces" ,
@@ -173,7 +173,7 @@ open class CoderRestClient(
173173 * @throws [APIResponseException].
174174 */
175175 suspend fun workspace (workspaceID : UUID ): Workspace {
176- val workspaceResponse = retroRestClient.workspace(workspaceID)
176+ val workspaceResponse = callWithRetry { retroRestClient.workspace(workspaceID) }
177177 if (! workspaceResponse.isSuccessful) {
178178 throw APIResponseException (
179179 " retrieve workspace" ,
@@ -196,8 +196,9 @@ open class CoderRestClient(
196196 * @throws [APIResponseException].
197197 */
198198 suspend fun resources (workspace : Workspace ): List <WorkspaceResource > {
199- val resourcesResponse =
199+ val resourcesResponse = callWithRetry {
200200 retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID)
201+ }
201202 if (! resourcesResponse.isSuccessful) {
202203 throw APIResponseException (
203204 " retrieve resources for ${workspace.name} " ,
@@ -213,7 +214,7 @@ open class CoderRestClient(
213214 }
214215
215216 suspend fun buildInfo (): BuildInfo {
216- val buildInfoResponse = retroRestClient.buildInfo()
217+ val buildInfoResponse = callWithRetry { retroRestClient.buildInfo() }
217218 if (! buildInfoResponse.isSuccessful) {
218219 throw APIResponseException (
219220 " retrieve build information" ,
@@ -232,7 +233,7 @@ open class CoderRestClient(
232233 * @throws [APIResponseException].
233234 */
234235 private suspend fun template (templateID : UUID ): Template {
235- val templateResponse = retroRestClient.template(templateID)
236+ val templateResponse = callWithRetry { retroRestClient.template(templateID) }
236237 if (! templateResponse.isSuccessful) {
237238 throw APIResponseException (
238239 " retrieve template with ID $templateID " ,
@@ -258,7 +259,7 @@ open class CoderRestClient(
258259 null ,
259260 WorkspaceBuildReason .JETBRAINS_CONNECTION
260261 )
261- val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
262+ val buildResponse = callWithRetry { retroRestClient.createWorkspaceBuild(workspace.id, buildRequest) }
262263 if (buildResponse.code() != HttpURLConnection .HTTP_CREATED ) {
263264 throw APIResponseException (
264265 " start workspace ${workspace.name} " ,
@@ -277,7 +278,7 @@ open class CoderRestClient(
277278 */
278279 suspend fun stopWorkspace (workspace : Workspace ): WorkspaceBuild {
279280 val buildRequest = CreateWorkspaceBuildRequest (null , WorkspaceTransition .STOP )
280- val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
281+ val buildResponse = callWithRetry { retroRestClient.createWorkspaceBuild(workspace.id, buildRequest) }
281282 if (buildResponse.code() != HttpURLConnection .HTTP_CREATED ) {
282283 throw APIResponseException (
283284 " stop workspace ${workspace.name} " ,
@@ -297,7 +298,7 @@ open class CoderRestClient(
297298 */
298299 suspend fun removeWorkspace (workspace : Workspace ) {
299300 val buildRequest = CreateWorkspaceBuildRequest (null , WorkspaceTransition .DELETE , false )
300- val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
301+ val buildResponse = callWithRetry { retroRestClient.createWorkspaceBuild(workspace.id, buildRequest) }
301302 if (buildResponse.code() != HttpURLConnection .HTTP_CREATED ) {
302303 throw APIResponseException (
303304 " delete workspace ${workspace.name} " ,
@@ -322,7 +323,7 @@ open class CoderRestClient(
322323 val template = template(workspace.templateID)
323324 val buildRequest =
324325 CreateWorkspaceBuildRequest (template.activeVersionID, WorkspaceTransition .START )
325- val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
326+ val buildResponse = callWithRetry { retroRestClient.createWorkspaceBuild(workspace.id, buildRequest) }
326327 if (buildResponse.code() != HttpURLConnection .HTTP_CREATED ) {
327328 throw APIResponseException (
328329 " update workspace ${workspace.name} " ,
@@ -337,6 +338,58 @@ open class CoderRestClient(
337338 }
338339 }
339340
341+ /* *
342+ * Executes a Retrofit call with a retry mechanism specifically for expired certificates.
343+ */
344+ private suspend fun <T > callWithRetry (block : suspend () -> Response <T >): Response <T > {
345+ return try {
346+ block()
347+ } catch (e: Exception ) {
348+ if (context.settingsStore.requiresMTlsAuth && isCertExpired(e)) {
349+ context.logger.info(" Certificate expired detected. Attempting refresh..." )
350+ if (refreshCertificates()) {
351+ context.logger.info(" Certificates refreshed, retrying the request..." )
352+ return block()
353+ }
354+ }
355+ throw e
356+ }
357+ }
358+
359+ private fun isCertExpired (e : Exception ): Boolean {
360+ return (e is javax.net.ssl.SSLHandshakeException || e is javax.net.ssl.SSLPeerUnverifiedException ) &&
361+ e.message?.contains(" certificate_expired" , ignoreCase = true ) == true
362+ }
363+
364+ private suspend fun refreshCertificates (): Boolean = withContext(Dispatchers .IO ) {
365+ val command = context.settingsStore.readOnly().tls.certRefreshCommand
366+ if (command.isNullOrBlank()) return @withContext false
367+
368+ return @withContext try {
369+ val result = ProcessExecutor ()
370+ .command(command.split(" " ).toList())
371+ .exitValueNormal()
372+ .readOutput(true )
373+ .execute()
374+
375+ if (result.exitValue == 0 ) {
376+ context.logger.info(" Certificate refresh successful. Reloading TLS and evicting pool." )
377+ tlsContext.reload()
378+
379+ // This is the "Magic Fix":
380+ // It forces OkHttp to close the broken HTTP/2 connection.
381+ httpClient.connectionPool.evictAll()
382+ return @withContext true
383+ } else {
384+ context.logger.error(" Refresh command failed with code ${result.exitValue} " )
385+ false
386+ }
387+ } catch (ex: Exception ) {
388+ context.logger.error(ex, " Failed to execute refresh command" )
389+ false
390+ }
391+ }
392+
340393 fun close () {
341394 httpClient.apply {
342395 dispatcher.executorService.shutdown()
0 commit comments