feat: Add GTIService to call GTI

This commit is contained in:
Jim Martens 2024-09-17 17:38:10 +02:00
parent 8d1e5ab3c6
commit 99136e5401
No known key found for this signature in database
GPG Key ID: CE04D842C9A68949
20 changed files with 3186 additions and 47 deletions

View File

@ -7,13 +7,14 @@
<module name="routing.server.main" />
<option name="SPRING_BOOT_MAIN_CLASS" value="de.hbt.routing.MainApplication" />
<extension name="net.ashald.envfile">
<option name="IS_ENABLED" value="false" />
<option name="IS_SUBST" value="false" />
<option name="IS_ENABLED" value="true" />
<option name="IS_SUBST" value="true" />
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
<option name="IS_IGNORE_MISSING_FILES" value="false" />
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
<ENTRIES>
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
<ENTRY IS_ENABLED="true" PARSER="env" IS_EXECUTABLE="false" PATH=".env" />
</ENTRIES>
</extension>
<method v="2">

View File

@ -16,4 +16,6 @@ dependencies {
implementation(libs.plugin.gradle.versions)
implementation(libs.plugin.version.catalog)
implementation(libs.plugin.jib)
implementation(libs.plugin.openapi.generator)
implementation(libs.swagger.parser)
}

View File

@ -0,0 +1,36 @@
import org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask
plugins {
id("org.openapi.generator")
kotlin("jvm")
}
openApiGenerate {
generatorName.set("kotlin")
inputSpec.set("$rootDir/module-server/src/main/resources/gtiApiDoc.yaml")
outputDir.set("${layout.buildDirectory.get()}/generated")
apiPackage.set("de.hbt.geofox.gti.api")
invokerPackage.set("de.hbt.geofox.gti.invoker")
modelPackage.set("de.hbt.geofox.gti.model")
configOptions.set(mapOf("serializationLibrary" to "jackson", "library" to "jvm-ktor"))
globalProperties.set(mapOf("models" to ""))
generateModelDocumentation.set(false)
}
sourceSets {
main {
kotlin.srcDir("${layout.buildDirectory.get()}/generated/src/main/kotlin")
}
}
openApiValidate {
inputSpec.set("$rootDir/module-server/src/main/resources/gtiApiDoc.yaml")
}
tasks.named("compileKotlin") {
dependsOn("openApiGenerate")
}
tasks.withType<KaptGenerateStubsTask>().configureEach {
dependsOn("openApiGenerate")
}

View File

@ -16,6 +16,7 @@ junit = "5.11.0"
assertj = "3.26.3"
mockito = "5.13.0"
keycloak = "25.0.4"
swagger-parser = "2.1.22"
kotlin-logging = "3.0.5"
kotlin-reflect = "2.0.20"
kotlin-lombok = "1.9.0"
@ -24,6 +25,7 @@ plugin-lombok = "8.0.1"
plugin-gradle-versions = "0.46.0"
plugin-version-catalog = "0.8.0"
plugin-kotlin-gradle = "1.9.21"
plugin-openapi-generator = "6.6.0"
plugin-jib = "3.4.3"
[libraries]
@ -44,6 +46,7 @@ spring-cloud-starter-bus-kafka = { module = "org.springframework.cloud:spring-cl
spring-cloud-starter-config = { module = "org.springframework.cloud:spring-cloud-starter-config" }
spring-cloud-config-server = { module = "org.springframework.cloud:spring-cloud-config-server" }
spring-cloud-leader-election = { module = "org.springframework.cloud:spring-cloud-kubernetes-fabric8-leader" }
swagger-parser = { module = "io.swagger.parser.v3:swagger-parser", version.ref = "swagger-parser" }
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter" }
spring-grpc = { module = "net.devh:grpc-spring-boot-starter", version.ref = "spring-grpc" }
spring-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "spring-doc" }
@ -89,6 +92,7 @@ plugin-lombok = { module = "io.freefair.gradle:lombok-plugin", version.ref = "pl
plugin-gradle-versions = { module = "com.github.ben-manes:gradle-versions-plugin", version.ref = "plugin-gradle-versions" }
plugin-version-catalog = { module = "nl.littlerobots.vcu:plugin", version.ref = "plugin-version-catalog" }
plugin-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "plugin-kotlin-gradle" }
plugin-openapi-generator = { module = "org.openapitools:openapi-generator-gradle-plugin", version.ref = "plugin-openapi-generator" }
plugin-jib = { module = "com.google.cloud.tools:jib-gradle-plugin", version.ref = "plugin-jib" }
[bundles]

View File

@ -1,6 +1,7 @@
plugins {
id("hbt.spring-boot-cloud-application")
id("hbt.kotlin")
id("hbt.openapi")
kotlin("kapt")
}

View File

@ -1,18 +0,0 @@
package de.hbt.routing
import de.hbt.routing.service.GTIWrapper
import org.springframework.http.HttpEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.client.RestTemplate
@RestController
open class GTIProxyController(private val restTemplate: RestTemplate,
private val gtiWrapper: GTIWrapper) {
@PostMapping(path = ["/gti/{requestMethod}"])
fun requestGti(@PathVariable("requestMethod") method: String, @RequestBody body: String, @RequestHeader("api-key") key: String): String {
val entity: HttpEntity<String> = gtiWrapper.sign(body, key)
val result = restTemplate.postForEntity("https://gti.geofox.de/gti/public/${method}", entity, String::class.java)
return result.body ?: ""
}
}

View File

@ -1,15 +1,13 @@
package de.hbt.routing.configuration
import de.hbt.routing.property.ServiceProperties
import de.hbt.support.property.HealthCheckProperties
import de.hbt.support.property.RestTemplateTimeoutProperties
import de.hbt.support.property.StatusProbeProperties
import de.hbt.support.property.TimeProperties
import de.hbt.support.property.*
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration
@Configuration
@EnableConfigurationProperties(RestTemplateTimeoutProperties::class,
StatusProbeProperties::class, TimeProperties::class,
HealthCheckProperties::class, ServiceProperties::class)
HealthCheckProperties::class, ServiceProperties::class,
GtiProperties::class)
open class PropertyConfiguration

View File

@ -0,0 +1,36 @@
package de.hbt.routing.gti
import de.hbt.geofox.gti.model.*
import org.springframework.stereotype.Service
import org.springframework.web.client.RestClient
private const val GTI_PREFIX = "https://gti.geofox.de/gti/public"
@Service
open class GTIService(private val restClient: RestClient) {
fun getRoute(start: SDName, destination: SDName, dateTime: GTITime): GRResponse {
val request = GRRequest(
start = start,
dest = destination,
time = dateTime,
schedulesBefore = 0,
schedulesAfter = 0,
version = 58
)
val result = restClient.post()
.uri("$GTI_PREFIX/getRoute")
.body(request)
.retrieve()
return result.body(GRResponse::class.java)!!
}
fun init(): InitResponse {
val request = InitRequest()
val result = restClient.post()
.uri("$GTI_PREFIX/init")
.body(request)
.retrieve()
return result.body(InitResponse::class.java)!!
}
}

View File

@ -0,0 +1,36 @@
package de.hbt.routing.gti
import de.hbt.geofox.gti.model.InitResponse
import io.swagger.v3.oas.annotations.Hidden
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping(value = ["/gti"])
@Tag(name = "Gti", description = "all requests that proxy GTI")
class GtiProxyController(private val gtiService: GTIService) {
@Operation(
summary = "Perform GTI init request",
responses = [ApiResponse(
responseCode = "200",
description = "Init request successful",
content = [Content(
schema = Schema(implementation = InitResponse::class)
)]
)]
)
@Hidden
@GetMapping("/init")
fun init(): ResponseEntity<InitResponse> {
val initResponse = gtiService.init()
return ResponseEntity.ok(initResponse)
}
}

View File

@ -82,6 +82,9 @@ resttemplate:
connectionRestTemplateTimeoutInMillis: 5000
de.hbt.support.health.greeting: "Good morning"
de.hbt.support.gti:
user: hbt47
secret: ${GTI_CLIENT_SECRET}
time:
defaultTimeZone: Europe/Berlin

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
package de.hbt.support.configuration
import de.hbt.support.interceptor.GTIInterceptor
import de.hbt.support.interceptor.HeaderInterceptorRest
import de.hbt.support.interceptor.LoggingInterceptorRest
import de.hbt.support.property.GtiProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
@ -17,4 +19,9 @@ open class InterceptorConfiguration {
open fun headerInterceptorRest(): HeaderInterceptorRest {
return HeaderInterceptorRest()
}
@Bean
open fun gtiInterceptor(properties: GtiProperties): GTIInterceptor {
return GTIInterceptor(properties)
}
}

View File

@ -1,5 +1,6 @@
package de.hbt.support.configuration
import de.hbt.support.interceptor.GTIInterceptor
import de.hbt.support.interceptor.HeaderInterceptorRest
import de.hbt.support.interceptor.LoggingInterceptorRest
import de.hbt.support.property.RestTemplateTimeoutProperties
@ -14,11 +15,12 @@ open class RestClientConfiguration {
open fun restClient(
headerInterceptorRest: HeaderInterceptorRest,
loggingInterceptor: LoggingInterceptorRest,
gtiInterceptor: GTIInterceptor,
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
): RestClient {
return RestClient.builder()
.messageConverters { it.add(Jaxb2RootElementHttpMessageConverter()) }
.requestInterceptors { listOf(headerInterceptorRest, loggingInterceptor) }
.requestInterceptors { it.addAll(listOf(headerInterceptorRest, gtiInterceptor, loggingInterceptor)) }
.build()
}
}

View File

@ -1,5 +1,6 @@
package de.hbt.support.configuration
import de.hbt.support.interceptor.GTIInterceptor
import de.hbt.support.interceptor.HeaderInterceptorRest
import de.hbt.support.interceptor.LoggingInterceptorRest
import de.hbt.support.property.RestTemplateTimeoutProperties
@ -14,10 +15,11 @@ open class RestTemplateConfiguration {
open fun restTemplate(
headerInterceptorRest: HeaderInterceptorRest,
loggingInterceptor: LoggingInterceptorRest,
gtiInterceptor: GTIInterceptor,
restTemplateTimeoutProperties: RestTemplateTimeoutProperties
): RestTemplate {
return RestTemplateBuilder()
.additionalInterceptors(headerInterceptorRest, loggingInterceptor)
.additionalInterceptors(headerInterceptorRest, loggingInterceptor, gtiInterceptor)
.setConnectTimeout(restTemplateTimeoutProperties.connectionRestTemplateTimeoutInMillis)
.setReadTimeout(restTemplateTimeoutProperties.readTimeoutRestTemplateInMillis)
.build()

View File

@ -14,7 +14,7 @@ import java.nio.file.Paths
import java.util.jar.JarFile
@Controller
@RequestMapping(value = ["/timetable"])
@RequestMapping(value = ["/routing"])
class VersionHtmlController {
@GetMapping(path = ["/version"])
fun version(): String {

View File

@ -1,10 +1,11 @@
package de.hbt.routing.service
package de.hbt.support.interceptor
import de.hbt.support.property.GtiProperties
import mu.KotlinLogging
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.stereotype.Service
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import java.nio.charset.StandardCharsets
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
@ -12,26 +13,20 @@ import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
private val logger = KotlinLogging.logger {}
class GTIInterceptor(private val properties: GtiProperties) : ClientHttpRequestInterceptor {
@Service
class GTIWrapper {
fun sign(body: String, key: String): HttpEntity<String> {
val headers = HttpHeaders()
headers.add("geofox-auth-user", "hbt47")
headers.add("geofox-auth-signature", computeHmacSHA1(body, key))
headers.contentType = MediaType.APPLICATION_JSON
val entity: HttpEntity<String> = HttpEntity(body, headers)
return entity
override fun intercept(request: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse {
request.headers.set("geofox-auth-user", properties.user)
request.headers.set("geofox-auth-signature", computeHmacSHA1(body, properties.secret))
return execution.execute(request, body)
}
private fun computeHmacSHA1(body: String, key: String): String {
private fun computeHmacSHA1(body: ByteArray, key: String): String {
try {
val mac = Mac.getInstance("HmacSHA1")
val secretKeySpec = SecretKeySpec(key.toByteArray(StandardCharsets.UTF_8), "HmacSHA1")
mac.init(secretKeySpec)
val digest = mac.doFinal(body.toByteArray(StandardCharsets.UTF_8))
val digest = mac.doFinal(body)
return Base64.getEncoder().encodeToString(digest)
} catch (e: IllegalArgumentException) {
logger.error("Error computing hmac", e)
@ -47,4 +42,8 @@ class GTIWrapper {
return ""
}
}
}
companion object {
private val logger = KotlinLogging.logger {}
}
}

View File

@ -0,0 +1,12 @@
package de.hbt.support.property
import io.swagger.v3.oas.annotations.media.Schema
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.ConstructorBinding
@ConfigurationProperties(prefix = "de.hbt.support.gti")
@Schema(description = "Properties, to configure GTI service")
data class GtiProperties @ConstructorBinding constructor(
val user: String,
val secret: String
)