diff --git a/README.md b/README.md index d59de2f3..3dad1efe 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,23 @@ fints4k uses slf4j as logging facade. So you can use any logger that supports slf4j, like Logback and log4j, to configure and get fints4k's log output. + +## Sample applications + +### WebApp + +Directly requesting bank servers is forbidden in browsers due to CORS. + +In order to use fints4k directly in browser you need a CORS proxy like the one from CorsProxy +[Application.kt](SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/Application.kt) or https://github.com/Rob--W/cors-anywhere. + +Set CORS proxy's URL in WebApp [main.kt](SampleApplications/WebApp/src/main/kotlin/main.kt). + +Start sample WebApp then with +```shell + ./gradlew WebApp:run --continuous +``` + ## License -fints4k is dual licensed as [AGPL](LICENSE.md) / commercial software. - -AGPL is a free open source software license. - -If you want to use it in closed source applications [contact](mailto:sales@dankito.net) us. \ No newline at end of file +Not free for commercial applications. More details to follow or [contact](mailto:sales@codinux.net) us. \ No newline at end of file diff --git a/SampleApplications/CorsProxy/build.gradle b/SampleApplications/CorsProxy/build.gradle new file mode 100644 index 00000000..967af15b --- /dev/null +++ b/SampleApplications/CorsProxy/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' +} + + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib" + + implementation "com.squareup.okhttp3:okhttp:4.9.3" + + implementation "org.slf4j:slf4j-simple:1.7.32" +} \ No newline at end of file diff --git a/SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/Application.kt b/SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/Application.kt new file mode 100644 index 00000000..16459de8 --- /dev/null +++ b/SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/Application.kt @@ -0,0 +1,5 @@ +package net.codinux.web.cors + +fun main() { + CorsProxyServer().start() +} \ No newline at end of file diff --git a/SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/CorsProxyServer.kt b/SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/CorsProxyServer.kt new file mode 100644 index 00000000..271ea370 --- /dev/null +++ b/SampleApplications/CorsProxy/src/main/kotlin/net/codinux/web/cors/CorsProxyServer.kt @@ -0,0 +1,90 @@ +package net.codinux.web.cors + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer +import okhttp3.Headers.Companion.toHeaders +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.slf4j.LoggerFactory +import java.net.InetSocketAddress + + +class CorsProxyServer { + + private val client = OkHttpClient() + + private val log = LoggerFactory.getLogger(CorsProxyServer::class.java) + + + fun start(port: Int = 8082) { + val server = HttpServer.create(InetSocketAddress("0.0.0.0", port), 0) + server.createContext("/") { httpExchange -> handleRequest(httpExchange) } + server.start() + + log.info("Started CORS proxy at port $port") + } + + + private fun handleRequest(exchange: HttpExchange) { + try { + setCorsHeaders(exchange) + + // Pre-flight request. Reply successfully: + if (exchange.requestMethod == "OPTIONS") { + exchange.sendResponseHeaders(200, -1) + return + } + + proxyCall(exchange) + } catch (e: Exception) { + log.error("Could not proxy call to ${exchange.requestURI}", e) + + exchange.sendResponseHeaders(500, -1) + } + } + + private fun proxyCall(exchange: HttpExchange) { + log.info("Proxying call to ${exchange.requestURI}") + + var url = exchange.requestURI.toString() + if (url.startsWith("/")) { + url = url.substring(1) + } + + // filter out host header as otherwise OkHttp is trying to connect to this header + val headers = exchange.requestHeaders.mapNotNull { if (it.key.lowercase() == "host") null else it.key to it.value.first() }.toMap() + + // OkHttp throws an exception if for a GET request a request body gets set + val requestBody = if ("GET" == exchange.requestMethod.uppercase()) null else exchange.requestBody.readAllBytes()?.toRequestBody() + + val request = Request.Builder() + .url(url) + .method(exchange.requestMethod, requestBody) + .headers(headers.toHeaders()) + .build() + + client.newCall(request).execute().use { response -> + response.headers.forEach { header -> exchange.responseHeaders.add(header.first, header.second) } + + exchange.sendResponseHeaders(response.code, response.body?.contentLength() ?: -1) + + exchange.responseBody.buffered().use { responseBodyStream -> // we need to close exchange.responseBody otherwise response doesn't get send + response.body?.byteStream()?.buffered()?.copyTo(responseBodyStream) + } + } + } + + private fun setCorsHeaders(exchange: HttpExchange) { + exchange.responseHeaders.add("Access-Control-Allow-Origin", "*") + + exchange.responseHeaders.add("Access-Control-Allow-Credentials", "true") + + exchange.responseHeaders.add("Access-Control-Allow-Methods", exchange.requestHeaders["access-control-request-method"]?.first() ?: "OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE") + + exchange.responseHeaders.add("Access-Control-Allow-Headers", exchange.requestHeaders["access-control-request-headers"]?.first() ?: "*") + +// exchange.responseHeaders.add("access-control-expose-headers", exchange.requestHeaders.map { it.key }.joinToString(",")) // TODO: needed? + } + +} \ No newline at end of file diff --git a/SampleApplications/WebApp/src/main/kotlin/main.kt b/SampleApplications/WebApp/src/main/kotlin/main.kt index 6c63bd3f..ff6d55db 100644 --- a/SampleApplications/WebApp/src/main/kotlin/main.kt +++ b/SampleApplications/WebApp/src/main/kotlin/main.kt @@ -11,7 +11,8 @@ fun main() { render(document.getElementById("root")!!) { child(AccountTransactionsView::class) { attrs { - // to circumvent CORS we have to use a CORS proxy like https://github.com/Rob--W/cors-anywhere. Set CORS proxy's URL here + // to circumvent CORS we have to use a CORS proxy like the SampleApplications.CorsProxy Application.kt or + // https://github.com/Rob--W/cors-anywhere. Set CORS proxy's URL here client = FinTsClientDeprecated(SimpleFinTsClientCallback(), ProxyingWebClient("http://localhost:8082/", KtorWebClient())) } } diff --git a/settings.gradle b/settings.gradle index f855d204..bc6ad089 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,3 +23,7 @@ include "WebApp" findProject(":WebApp")?.projectDir = file("SampleApplications/WebApp") findProject(":WebApp")?.name = "WebApp" +include "CorsProxy" +findProject(":CorsProxy")?.projectDir = file("SampleApplications/CorsProxy") +findProject(":CorsProxy")?.name = "CorsProxy" +