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.
## 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.
Not free for commercial applications. More details to follow or [contact](mailto:sales@codinux.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")!!) {
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()))
}
}

View File

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