From c0555b53ed2ce78dd692d49f41d1cd86794200cd Mon Sep 17 00:00:00 2001 From: Aleksandr Krasilnikov Date: Wed, 20 May 2020 12:07:07 +0300 Subject: [PATCH] [space plugin] authorization: get rid of Ktor Server and implement simple socket auth. Better solution required GitOrigin-RevId: 086ac8ccf3c4d6f41a75a6b9c643bfa5369e6aa9 --- .../kotlin/circlet/auth/IdeaAuthenticator.kt | 65 ---------------- .../components/CircletWorkspaceComponent.kt | 76 +++++++++++++++++-- 2 files changed, 68 insertions(+), 73 deletions(-) delete mode 100644 plugins/space/src/main/kotlin/circlet/auth/IdeaAuthenticator.kt diff --git a/plugins/space/src/main/kotlin/circlet/auth/IdeaAuthenticator.kt b/plugins/space/src/main/kotlin/circlet/auth/IdeaAuthenticator.kt deleted file mode 100644 index 63c857f62b79..000000000000 --- a/plugins/space/src/main/kotlin/circlet/auth/IdeaAuthenticator.kt +++ /dev/null @@ -1,65 +0,0 @@ -package circlet.auth - -import circlet.common.oauth.* -import circlet.platform.api.oauth.* -import circlet.platform.workspaces.* -import io.ktor.application.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* -import io.ktor.server.engine.* -import io.ktor.server.jetty.* -import libraries.coroutines.extra.* -import libraries.klogging.* -import runtime.utils.* -import java.awt.* -import java.net.* -import java.util.concurrent.* -import kotlin.coroutines.* -import kotlinx.coroutines.* - -val log = KLoggers.logger() - -private val ports = lazy { - val regex = "[^\\d]*(?\\d+)[^\\d]*".toRegex() - val ports = IdeaOAuthConfig.redirectURIs.mapNotNull { it -> - regex.find(it)?.groups?.get("port")?.value?.toIntOrNull() - } - Pair(ports.min() ?: 1000, ports.max() ?: 65535) -} - -suspend fun accessTokenInteractive(lifetime: Lifetime, config: WorkspaceConfiguration): OAuthTokenResponse { - val port = selectFreePort(ports.value.first, ports.value.second) - val codeFlow = CodeFlowConfig(config, "http://localhost:$port/auth") - - return suspendCancellableCoroutine { cnt -> - - val server = try { - embeddedServer(Jetty, port, "localhost") { - routing { - get("auth") { - val token = codeFlow.handleCodeFlowRedirect(call.request.uri) - call.respondText("", io.ktor.http.ContentType("text", "html")) - cnt.resume(token) - } - } - }.start(wait = false) - } catch (th: Throwable) { - log.error(th, "Can't start server at: ${codeFlow.redirectUri}") - throw th - } - - lifetime.add { - server.stop(100, 5000, TimeUnit.MILLISECONDS) - } - try { - val uri = URI(codeFlow.codeFlowURL()) - Desktop.getDesktop().browse(uri) - } catch (th: Throwable) { - val message = "Can't open '${config.server}' in system browser" - log.warn(th, message) - cnt.resume(OAuthTokenResponse.Error(config.server, "", "Can't open '${config.server}' in system browser.")) - } - } -} - diff --git a/plugins/space/src/main/kotlin/circlet/components/CircletWorkspaceComponent.kt b/plugins/space/src/main/kotlin/circlet/components/CircletWorkspaceComponent.kt index a6824249d76b..818e61420673 100644 --- a/plugins/space/src/main/kotlin/circlet/components/CircletWorkspaceComponent.kt +++ b/plugins/space/src/main/kotlin/circlet/components/CircletWorkspaceComponent.kt @@ -1,7 +1,6 @@ package circlet.components import circlet.arenas.initCircletArenas -import circlet.auth.accessTokenInteractive import circlet.client.api.impl.ApiClassesDeserializer import circlet.common.oauth.IdeaOAuthConfig import circlet.permission.FeatureFlagsVmPersistenceKey @@ -9,6 +8,7 @@ import circlet.platform.api.oauth.OAuthTokenResponse import circlet.platform.api.oauth.toTokenInfo import circlet.platform.api.serialization.ExtendableSerializationRegistry import circlet.platform.client.ConnectionStatus +import circlet.platform.workspaces.CodeFlowConfig import circlet.platform.workspaces.WorkspaceConfiguration import circlet.platform.workspaces.WorkspaceManagerHost import circlet.runtime.ApplicationDispatcher @@ -23,8 +23,12 @@ import circlet.workspaces.Workspace import circlet.workspaces.WorkspaceManager import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.ServiceManager +import com.intellij.util.concurrency.AppExecutorUtil +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.suspendCancellableCoroutine import libraries.coroutines.extra.Lifetime import libraries.coroutines.extra.launch +import libraries.klogging.KLogger import libraries.klogging.assert import libraries.klogging.logger import runtime.Ui @@ -33,11 +37,19 @@ import runtime.persistence.InMemoryPersistence import runtime.persistence.PersistenceConfiguration import runtime.persistence.PersistenceKey import runtime.reactive.* - -private val log = logger() +import runtime.utils.selectFreePort +import java.awt.Desktop +import java.io.BufferedReader +import java.io.InputStreamReader +import java.io.PrintWriter +import java.net.ServerSocket +import java.net.Socket +import java.net.URI +import kotlin.coroutines.resume // monitors CircletConfigurable state, creates and exposed instance of Workspace, provides various state properties and callbacks. class CircletWorkspaceComponent : WorkspaceManagerHost(), LifetimedDisposable by LifetimedDisposableImpl() { + private val log: KLogger = logger() private val ideaClientPersistenceConfiguration = PersistenceConfiguration( FeatureFlagsVmPersistenceKey, @@ -45,6 +57,7 @@ class CircletWorkspaceComponent : WorkspaceManagerHost(), LifetimedDisposable by ) private val workspacesLifetimes = SequentialLifetimes(lifetime) + private val manager = mutableProperty(null) val workspace = flatMapInit(manager, null) { @@ -75,9 +88,6 @@ class CircletWorkspaceComponent : WorkspaceManagerHost(), LifetimedDisposable by private fun initApp() { val application = ApplicationManager.getApplication() - //if (!application.isUnitTestMode && !application.isHeadlessEnvironment) { - // KLoggerStaticFactory.customFactory = KLoggerFactoryIdea - //} mutableUiDispatch = ApplicationDispatcher(application) @@ -92,13 +102,63 @@ class CircletWorkspaceComponent : WorkspaceManagerHost(), LifetimedDisposable by manager.value?.signOut(false) } + private val ports = lazy { + val regex = "[^\\d]*(?\\d+)[^\\d]*".toRegex() + val ports = IdeaOAuthConfig.redirectURIs.mapNotNull { it -> + regex.find(it)?.groups?.get("port")?.value?.toIntOrNull() + } + Pair(ports.min() ?: 1000, ports.max() ?: 65535) + } + suspend fun signIn(lifetime: Lifetime, server: String): OAuthTokenResponse { log.assert(manager.value == null, "manager.value == null") val lt = workspacesLifetimes.next() val wsConfig = ideaConfig(server) - val wss = WorkspaceManager(lt, null, this, InMemoryPersistence(), IdeaPasswordSafePersistence, ideaClientPersistenceConfiguration, wsConfig) - val response = accessTokenInteractive(lifetime, wsConfig) + val wss = WorkspaceManager(lt, null, this, InMemoryPersistence(), IdeaPasswordSafePersistence, ideaClientPersistenceConfiguration, + wsConfig) + + val port = selectFreePort(ports.value.first, ports.value.second) + val authUrl = "http://localhost:$port/auth" + val codeFlow = CodeFlowConfig(wsConfig, authUrl) + + val response: OAuthTokenResponse = suspendCancellableCoroutine { cnt -> + launch(lifetime, AppExecutorUtil.getAppExecutorService().asCoroutineDispatcher()) { + ServerSocket(port).use { serverSocket -> + val socket: Socket = serverSocket.accept() + socket.getInputStream().use { inputStream -> + BufferedReader(InputStreamReader(inputStream)).use { reader -> + var line = reader.readLine() + + line = line.substringAfter("/").substringBefore(" ") + val token = codeFlow.handleCodeFlowRedirect("/$line") + cnt.resume(token) + + PrintWriter(socket.getOutputStream()).use { out -> + val response = "" + out.println("HTTP/1.1 200 OK") + out.println("Content-Type: text/html") + out.println("Content-Length: " + response.length) + out.println() + out.println(response) + out.flush() + } + } + } + } + } + + try { + val uri = URI(codeFlow.codeFlowURL()) + Desktop.getDesktop().browse(uri) + } + catch (th: Throwable) { + val message = "Can't open '${wsConfig.server}' in system browser" + log.warn(th, message) + cnt.resume(OAuthTokenResponse.Error(wsConfig.server, "", "Can't open '${wsConfig.server}' in system browser.")) + } + } + if (response is OAuthTokenResponse.Success) { log.info { "response = ${response.accessToken} ${response.expiresIn} ${response.refreshToken} ${response.scope}" } wss.signInWithToken(response.toTokenInfo())