Files
myice-kmp/composeApp/src/desktopMain/kotlin/ch/parano/myice/auth/DesktopOAuthClient.kt
T
herel ed97b2a1f0 license: add GPL-3.0-or-later license
- Add full LICENSE file (GPL-3.0)
- Add SPDX headers to all 44 Kotlin source files
- Add SPDX headers to 3 iOS Swift/Info.plist files
- Update README.md and AGENTS.md with license info
2026-07-09 16:19:38 +02:00

116 lines
4.3 KiB
Kotlin

// 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 <https://www.gnu.org/licenses/>.
package ch.parano.myice.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<String?>()
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 = """
<html><body>
<script>
var hash = window.location.hash.substring(1);
window.location.href = "/token?" + hash;
</script>
</body></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("<html><body><h1>Authentication complete. You can close this window.</h1></body></html>")
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()
}
}
}