Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ocpi-2.2.1): Add Content-Type header on requests (if needed), on all responses & fixed headers case sensitivity #14

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@ import com.fasterxml.jackson.core.type.TypeReference
import com.izivia.ocpi.toolkit.modules.credentials.repositories.PlatformRepository
import com.izivia.ocpi.toolkit.modules.versions.domain.ModuleID
import com.izivia.ocpi.toolkit.transport.TransportClientBuilder
import com.izivia.ocpi.toolkit.transport.domain.HttpException
import com.izivia.ocpi.toolkit.transport.domain.HttpRequest
import com.izivia.ocpi.toolkit.transport.domain.HttpResponse
import com.izivia.ocpi.toolkit.transport.domain.HttpStatus
import com.izivia.ocpi.toolkit.transport.domain.*
import java.util.*

typealias AuthenticatedHttpRequest = HttpRequest

object Header {
const val AUTHORIZATION = "Authorization"
const val X_REQUEST_ID = "X-Request-ID"
const val X_CORRELATION_ID = "X-Correlation-ID"
const val X_TOTAL_COUNT = "X-Total-Count"
const val X_LIMIT = "X-Limit"
const val LINK = "Link"
const val CONTENT_TYPE = "Content-Type"
}

object ContentType {
const val APPLICATION_JSON = "application/json"
}

/**
* Parse body of a paginated request. The result will be stored in a SearchResult which contains all pagination
* information.
Expand All @@ -22,10 +33,10 @@ inline fun <reified T> HttpResponse.parsePaginatedBody(offset: Int): OcpiRespons
.let { parsedBody ->
OcpiResponseBody(
data = parsedBody.data?.toSearchResult(
totalCount = headers["X-Total-Count"]!!.toInt(),
limit = headers["X-Limit"]!!.toInt(),
totalCount = getHeader(Header.X_TOTAL_COUNT)!!.toInt(),
limit = getHeader(Header.X_LIMIT)!!.toInt(),
lilgallon marked this conversation as resolved.
Show resolved Hide resolved
offset = offset,
nextPageUrl = headers["Link"]?.split("<")?.get(1)?.split(">")?.first()
nextPageUrl = getHeader(Header.LINK)?.split("<")?.get(1)?.split(">")?.first()
),
status_code = parsedBody.status_code,
status_message = parsedBody.status_message,
Expand Down Expand Up @@ -56,7 +67,7 @@ fun String.decodeBase64(): String = Base64.getDecoder().decode(this).decodeToStr
/**
* Creates the authorization header from the given token
*/
fun authorizationHeader(token: String): Pair<String, String> = "Authorization" to "Token ${token.encodeBase64()}"
fun authorizationHeader(token: String): Pair<String, String> = Header.AUTHORIZATION to "Token ${token.encodeBase64()}"

/**
* Creates the authorization header by taking the right token in the platform repository
Expand Down Expand Up @@ -106,27 +117,42 @@ suspend fun HttpRequest.authenticate(
fun HttpRequest.authenticate(token: String): AuthenticatedHttpRequest =
withHeaders(headers = headers.plus(authorizationHeader(token = token)))

/**
* It adds Content-Type header as "application/json" if the body is not null.
*/
private fun HttpRequest.withContentTypeHeaderIfNeeded(): HttpRequest =
withHeaders(
headers = if (body != null) {
headers.plus(Header.CONTENT_TYPE to ContentType.APPLICATION_JSON)
} else {
headers
}
)

/**
* For debugging issues, OCPI implementations are required to include unique IDs via HTTP headers in every
* request/response
* request/response.
*
* - X-Request-ID: Every request SHALL contain a unique request ID, the response to this request SHALL contain the same
* ID.
* - X-Correlation-ID: Every request/response SHALL contain a unique correlation ID, every response to this request
* SHALL contain the same ID.
*
* Moreover, for requests, Content-Type SHALL be set to application/json for any request that contains a
* message body: POST, PUT and PATCH. When no body is present, probably in a GET or DELETE, then the Content-Type
* header MAY be omitted.
*
* This method should be called when doing the first request from a client.
*
* Dev note: When the server does a request (not a response), it must keep the same X-Correlation-ID but generate a new
* X-Request-ID. So don't call this method in that case.
*/
fun HttpRequest.withDebugHeaders(): HttpRequest =
fun HttpRequest.withRequiredHeaders(): HttpRequest =
withHeaders(
headers = headers
.plus("X-Request-ID" to generateUUIDv4Token())
.plus("X-Correlation-ID" to generateUUIDv4Token())
)
.plus(Header.X_REQUEST_ID to generateUUIDv4Token())
.plus(Header.X_CORRELATION_ID to generateUUIDv4Token())
).withContentTypeHeaderIfNeeded()

/**
* For debugging issues, OCPI implementations are required to include unique IDs via HTTP headers in every
Expand All @@ -137,23 +163,27 @@ fun HttpRequest.withDebugHeaders(): HttpRequest =
* - X-Correlation-ID: Every request/response SHALL contain a unique correlation ID, every response to this request
* SHALL contain the same ID.
*
* Moreover, for requests, Content-Type SHALL be set to application/json for any request that contains a
* message body: POST, PUT and PATCH. When no body is present, probably in a GET or DELETE, then the Content-Type
* header MAY be omitted.
*
* This method should be called when doing the a request from a server.
*
* TODO: test debug headers with integration tests
*
* @param headers Headers of the caller. It will re-use the X-Correlation-ID header and regenerate X-Request-ID
*/
fun HttpRequest.withUpdatedDebugHeaders(headers: Map<String, String>): HttpRequest =
fun HttpRequest.withUpdatedRequiredHeaders(headers: Map<String, String>): HttpRequest =
withHeaders(
headers = headers
.plus("X-Request-ID" to generateUUIDv4Token())
.plus(Header.X_REQUEST_ID to generateUUIDv4Token())
.plus(
"X-Correlation-ID" to headers.getOrDefault(
"X-Correlation-ID",
"error - could not get X-Correlation-ID header"
)
Header.X_CORRELATION_ID to (
getHeader(Header.X_CORRELATION_ID)
lilgallon marked this conversation as resolved.
Show resolved Hide resolved
?: "error - could not get ${Header.X_CORRELATION_ID} header"
)
)
)
).withContentTypeHeaderIfNeeded()

/**
* For debugging issues, OCPI implementations are required to include unique IDs via HTTP headers in every
Expand All @@ -167,24 +197,36 @@ fun HttpRequest.withUpdatedDebugHeaders(headers: Map<String, String>): HttpReque
* This method should be called when responding to a request from a client.
*/
fun HttpRequest.getDebugHeaders() = listOfNotNull(
headers["X-Request-ID"]?.let { "X-Request-ID" to it },
headers["X-Correlation-ID"]?.let { "X-Correlation-ID" to it }
getHeader(Header.X_REQUEST_ID)?.let { Header.X_REQUEST_ID to it },
getHeader(Header.X_CORRELATION_ID)?.let { Header.X_CORRELATION_ID to it }
).toMap()

/**
* Returns the value of a header by its key. The key is not case-sensitive.
*/
fun HttpRequest.getHeader(key: String): String? =
headers.mapKeys { it.key.lowercase() }[key.lowercase()]

/**
* Returns the value of a header by its key. The key is not case-sensitive.
*/
fun HttpResponse.getHeader(key: String): String? =
headers.mapKeys { it.key.lowercase() }[key.lowercase()]

/**
* Parses authorization header from the HttpRequest
*
* @throws OcpiClientNotEnoughInformationException if the token is missing
* @throws HttpException if the authorization header is missing
*/
fun HttpRequest.parseAuthorizationHeader() = (headers["Authorization"] ?: headers["authorization"])
fun HttpRequest.parseAuthorizationHeader() = getHeader(Header.AUTHORIZATION)
?.let {
if (it.startsWith("Token ")) it
else throw OcpiClientInvalidParametersException("Unkown token format: $it")
}
?.removePrefix("Token ")
?.decodeBase64()
?: throw HttpException(HttpStatus.UNAUTHORIZED, "Authorization header missing")
?: throw HttpException(HttpStatus.UNAUTHORIZED, "${Header.AUTHORIZATION} header missing")

/**
* Throws an exception if the token is invalid. Does nothing otherwise.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ suspend fun <T> HttpRequest.httpResponse(fn: suspend () -> OcpiResponseBody<T>):
}
),
headers = getDebugHeaders()
.plus(Header.CONTENT_TYPE to ContentType.APPLICATION_JSON)
).let {
if (isPaginated) {
it.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class CredentialsClient(
transportClient
.send(
HttpRequest(method = HttpMethod.GET)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(token = tokenC)
)
.parseBody()
Expand All @@ -31,7 +31,7 @@ class CredentialsClient(
method = HttpMethod.POST,
body = mapper.writeValueAsString(credentials)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(token = tokenA)
)
.parseBody()
Expand All @@ -43,7 +43,7 @@ class CredentialsClient(
method = HttpMethod.PUT,
body = mapper.writeValueAsString(credentials)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(token = tokenC)
)
.parseBody()
Expand All @@ -52,7 +52,7 @@ class CredentialsClient(
transportClient
.send(
HttpRequest(method = HttpMethod.DELETE)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(token = tokenC)
)
.parseBody()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class CredentialsServerService(
.build(credentials.url)
.send(
HttpRequest(method = HttpMethod.GET)
.withUpdatedDebugHeaders(headers = debugHeaders)
.withUpdatedRequiredHeaders(headers = debugHeaders)
.authenticate(token = credentials.token)
)
.parseBody<OcpiResponseBody<List<Version>>>()
Expand All @@ -126,7 +126,7 @@ class CredentialsServerService(
.build(matchingVersion.url)
.send(
HttpRequest(method = HttpMethod.GET)
.withUpdatedDebugHeaders(headers = debugHeaders)
.withUpdatedRequiredHeaders(headers = debugHeaders)
.authenticate(token = credentials.token)
)
.parseBody<OcpiResponseBody<VersionDetails>>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class LocationsCpoClient(
method = HttpMethod.GET,
path = "/$countryCode/$partyId/$locationId"
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -56,7 +56,7 @@ class LocationsCpoClient(
method = HttpMethod.GET,
path = "/$countryCode/$partyId/$locationId/$evseUid"
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -74,7 +74,7 @@ class LocationsCpoClient(
method = HttpMethod.GET,
path = "/$countryCode/$partyId/$locationId/$evseUid/$connectorId"
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -92,7 +92,7 @@ class LocationsCpoClient(
path = "/$countryCode/$partyId/$locationId",
body = mapper.writeValueAsString(location)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -111,7 +111,7 @@ class LocationsCpoClient(
path = "/$countryCode/$partyId/$locationId/$evseUid",
body = mapper.writeValueAsString(evse)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -131,7 +131,7 @@ class LocationsCpoClient(
path = "/$countryCode/$partyId/$locationId/$evseUid/$connectorId",
body = mapper.writeValueAsString(connector)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -149,7 +149,7 @@ class LocationsCpoClient(
path = "/$countryCode/$partyId/$locationId",
body = mapper.writeValueAsString(location)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -168,7 +168,7 @@ class LocationsCpoClient(
path = "/$countryCode/$partyId/$locationId",
body = mapper.writeValueAsString(evse)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -188,7 +188,7 @@ class LocationsCpoClient(
path = "/$countryCode/$partyId/$locationId",
body = mapper.writeValueAsString(connector)
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class LocationsEmspClient(
limit?.let { "limit" to limit.toString() }
).toMap()
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parsePaginatedBody(offset)
Expand All @@ -60,7 +60,7 @@ class LocationsEmspClient(
method = HttpMethod.GET,
path = "/$locationId",
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -72,7 +72,7 @@ class LocationsEmspClient(
method = HttpMethod.GET,
path = "/$locationId/$evseUid",
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -88,7 +88,7 @@ class LocationsEmspClient(
method = HttpMethod.GET,
path = "/$locationId/$evseUid/$connectorId",
)
.withDebugHeaders()
.withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class SessionsCpoClient(
HttpRequest(
method = HttpMethod.GET,
path = "/$countryCode/$partyId/$sessionId"
).withDebugHeaders()
).withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -50,7 +50,7 @@ class SessionsCpoClient(
method = HttpMethod.PUT,
path = "/$countryCode/$partyId/$sessionId",
body = mapper.writeValueAsString(session)
).withDebugHeaders()
).withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand All @@ -67,7 +67,7 @@ class SessionsCpoClient(
method = HttpMethod.PATCH,
path = "/$countryCode/$partyId/$sessionId",
body = mapper.writeValueAsString(session)
).withDebugHeaders()
).withRequiredHeaders()
.authenticate(platformRepository = platformRepository, baseUrl = serverVersionsEndpointUrl)
)
.parseBody()
Expand Down
Loading