Implemented a CORS proxy server to circumvent CORS restriction in WebApp

This commit is contained in:
dankito 2022-02-17 02:46:47 +01:00
parent 51c09dab7b
commit 523e538ecb
6 changed files with 131 additions and 6 deletions

View File

@ -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. 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 ## License
fints4k is dual licensed as [AGPL](LICENSE.md) / commercial software. Not free for commercial applications. More details to follow or [contact](mailto:sales@codinux.net) us.
AGPL is a free open source software license.
If you want to use it in closed source applications [contact](mailto:sales@dankito.net) us.

View File

@ -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"
}

View File

@ -0,0 +1,5 @@
package net.codinux.web.cors
fun main() {
CorsProxyServer().start()
}

View File

@ -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?
}
}

View File

@ -11,7 +11,8 @@ fun main() {
render(document.getElementById("root")!!) { render(document.getElementById("root")!!) {
child(AccountTransactionsView::class) { child(AccountTransactionsView::class) {
attrs { 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())) client = FinTsClientDeprecated(SimpleFinTsClientCallback(), ProxyingWebClient("http://localhost:8082/", KtorWebClient()))
} }
} }

View File

@ -23,3 +23,7 @@ include "WebApp"
findProject(":WebApp")?.projectDir = file("SampleApplications/WebApp") findProject(":WebApp")?.projectDir = file("SampleApplications/WebApp")
findProject(":WebApp")?.name = "WebApp" findProject(":WebApp")?.name = "WebApp"
include "CorsProxy"
findProject(":CorsProxy")?.projectDir = file("SampleApplications/CorsProxy")
findProject(":CorsProxy")?.name = "CorsProxy"