- 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
116 lines
4.3 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
}
|