// SPDX-License-Identifier: GPL-3.0-or-later // MyIce Kotlin Multiplatform — schedule/convocation viewer // Copyright (C) 2026 parano.ch // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package ch.parano.myicek.auth import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.withTimeout import java.awt.Desktop import java.io.BufferedReader import java.io.InputStreamReader import java.io.PrintWriter import java.net.InetSocketAddress import java.net.ServerSocket import java.net.URI import java.net.URLEncoder import kotlin.concurrent.thread class DesktopOAuthClient : OAuthClient { override fun redirectUri(): String = "myice://callback" override suspend fun authenticate(loginUrl: String): String? { val server = ServerSocket() server.bind(InetSocketAddress("127.0.0.1", 0)) val serverPort = server.localPort val realRedirectUri = "http://localhost:$serverPort/callback" val fullLoginUrl = loginUrl.replace( "redirect_uri=myice://callback", "redirect_uri=" + URLEncoder.encode(realRedirectUri, "UTF-8") ) val deferred = CompletableDeferred() thread { try { while (true) { val socket = server.accept() val reader = BufferedReader(InputStreamReader(socket.getInputStream())) val writer = PrintWriter(socket.getOutputStream()) val requestLine = reader.readLine() ?: continue val path = requestLine.split(" ").getOrNull(1) ?: continue if (path.startsWith("/callback")) { val html = """ """.trimIndent() writer.print("HTTP/1.1 200 OK\r\n") writer.print("Content-Type: text/html\r\n") writer.print("Connection: close\r\n") writer.print("\r\n") writer.print(html) writer.flush() } else if (path.startsWith("/token")) { val query = path.substringAfter("?") val token = parseAccessTokenFromFragment(query) deferred.complete(token) writer.print("HTTP/1.1 200 OK\r\n") writer.print("Content-Type: text/html\r\n") writer.print("Connection: close\r\n") writer.print("\r\n") writer.print("

Authentication complete. You can close this window.

") writer.flush() socket.close() break } socket.close() } } catch (e: Exception) { deferred.complete(null) } finally { server.close() } } try { Desktop.getDesktop().browse(URI(fullLoginUrl)) } catch (e: Exception) { server.close() return null } return try { withTimeout(5 * 60 * 1000L) { deferred.await() } } catch (e: TimeoutCancellationException) { null } finally { server.close() } } }