All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.gematik.bbriccs.rest.vau.VauClient Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2024 gematik GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.gematik.bbriccs.rest.vau;

import static java.text.MessageFormat.format;

import de.gematik.bbriccs.rest.*;
import de.gematik.bbriccs.rest.headers.AuthHttpHeaderKey;
import de.gematik.bbriccs.rest.headers.HttpHeader;
import de.gematik.bbriccs.rest.headers.StandardHttpHeaderKey;
import de.gematik.bbriccs.rest.plugins.HttpBObserver;
import de.gematik.bbriccs.rest.plugins.HttpBRequestObserver;
import de.gematik.bbriccs.rest.plugins.HttpBResponseObserver;
import de.gematik.bbriccs.rest.tls.EmptyTrustManager;
import de.gematik.bbriccs.rest.vau.exceptions.MissingAuthorizationBearerException;
import de.gematik.bbriccs.rest.vau.exceptions.VauException;
import de.gematik.bbriccs.rest.vau.plugins.VauObserver;
import de.gematik.bbriccs.rest.vau.plugins.VauObserverManager;
import de.gematik.bbriccs.rest.vau.plugins.VauRequestObserver;
import de.gematik.bbriccs.rest.vau.plugins.VauResponseObserver;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.util.*;
import javax.crypto.BadPaddingException;
import javax.crypto.SecretKey;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import kong.unirest.core.HttpResponse;
import kong.unirest.core.RequestBodyEntity;
import kong.unirest.core.Unirest;
import kong.unirest.core.UnirestInstance;
import lombok.*;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class VauClient implements HttpBClient {

  private final UnirestInstance unirest;
  private final VauProtocol vauProtocol;
  private final RawHttpCodec rawHttpCodec;
  private final String fdBaseUrl;
  private final List staticHeader;

  private final VauObserverManager vauObserver;

  /**
   * UserPseudonym, which will be used for VAU-Sessions. Initially, each VAU-Session starts with
   * UserPseudonym equal to 0
   */
  private String vauUserPseudonym = "0";

  private VauClient(VauClientBuilder builder, VauProtocol vauProtocol) {
    this.fdBaseUrl = Objects.requireNonNull(builder.url, "VauClientBuilder is missing URL");
    this.unirest = Objects.requireNonNull(builder.unirest, "VauClientBuilder is missing Unirest");
    this.staticHeader =
        Objects.requireNonNull(builder.headers, "VauClientBuilder is missing static headers");
    this.rawHttpCodec =
        Objects.requireNonNull(builder.codec, "VauClientBuilder is missing raw HTTP codec");
    this.vauProtocol =
        Objects.requireNonNull(vauProtocol, "VauClientBuilder is missing VAU-Protocol");

    this.vauObserver = builder.observerBuilder.build();
  }

  @Override
  public void shutDown() {
    this.unirest.close();
  }

  public SecretKey symmetricKey() {
    return vauProtocol.getDecryptionKey();
  }

  @Override
  public HttpBResponse send(HttpBRequest bRequest) {
    this.vauObserver.serveRequestObservers(bRequest);
    val rawInnerHttp = rawHttpCodec.encode(bRequest).getBytes(StandardCharsets.UTF_8);

    val bearerToken =
        bRequest.getBearerToken().orElseThrow(MissingAuthorizationBearerException::new);
    val vauEncrypted = vauProtocol.encrypt(bearerToken, rawInnerHttp);
    this.vauObserver.serveRequestObservers(vauEncrypted);
    val vauRequest = this.createRequest(bRequest, vauEncrypted);

    log.info(
        "Send VAU-Request to: {} with Request ID {}",
        getVauRequestUrl(),
        vauEncrypted.requestIdAsString());
    val outerResponse = vauRequest.asBytes();
    val bResponse = createResponse(vauEncrypted, outerResponse);
    this.vauObserver.serveResponseObservers(bResponse);
    log.info(
        "Received VAU-Response with Status Code {} for Request ID {} with"
            + " VAU Userpseudonym: {}",
        outerResponse.getStatus(),
        vauEncrypted.requestIdAsString(),
        vauUserPseudonym);
    return bResponse;
  }

  private HttpBResponse createResponse(
      VauEncryptionEnvelope request, HttpResponse outerResponse) {
    this.extractUserPseudonym(outerResponse);

    // check if the response is octet-stream (encrypted) before decrypting
    val contentType =
        outerResponse.getHeaders().getFirst(StandardHttpHeaderKey.CONTENT_TYPE.getKey());
    val isOctetStream = Objects.requireNonNull(contentType).contains("octet-stream");
    if (isOctetStream) {
      return decryptResponse(request, outerResponse);
    } else {
      return nonEncryptedResponse(contentType, outerResponse);
    }
  }

  private HttpBResponse decryptResponse(
      VauEncryptionEnvelope request, HttpResponse outerResponse) {
    try {
      val decrypted = vauProtocol.decrypt(outerResponse.getBody());
      this.vauObserver.serveResponseObservers(
          new VauEncryptionEnvelope(
              request.vauVersion(),
              request.decryptionKey(),
              request.requestId(),
              request.accessToken(),
              decrypted));
      return rawHttpCodec.decodeResponse(decrypted);
    } catch (BadPaddingException e) {
      val innerHttp = outerResponse.getBody();
      val b64Body = Base64.getEncoder().encodeToString(innerHttp);
      log.error(
          format(
              "Error while decoding VAU inner-HTTP of length {0}\n{1}", innerHttp.length, b64Body));
      throw new VauException("Error while decoding VAU", e);
    }
  }

  /**
   * On rare occasions (usually on remote side errors) the backend sends non-encrypted messages
   * which will be handled here to be at least able to receive such responses
   *
   * @param contentType provided by remote
   * @param outerResponse received from remote
   * @return an extracted HttpBResponse
   */
  private HttpBResponse nonEncryptedResponse(
      String contentType, HttpResponse outerResponse) {
    log.warn(
        "Received VAU Response which seems to be not encrypted with content-type: '{}': forward"
            + " plain content:\n{}",
        contentType,
        new String(outerResponse.getBody()));

    val headers =
        outerResponse.getHeaders().all().stream()
            .map(h -> new HttpHeader(h.getName(), h.getValue()))
            .toList();
    return new HttpBResponse(
        HttpVersion.HTTP_1_1, outerResponse.getStatus(), headers, outerResponse.getBody());
  }

  /**
   * Extract the user pseudonym from response and store for next request to maintain the session
   *
   * @param outerResponse received from remote which provides the user pseudonym
   */
  private void extractUserPseudonym(HttpResponse outerResponse) {
    if (outerResponse.getHeaders().containsKey("Userpseudonym")) {
      vauUserPseudonym = outerResponse.getHeaders().getFirst("Userpseudonym");
    } else {
      // reset
      vauUserPseudonym = "0";
    }
  }

  private RequestBodyEntity createRequest(HttpBRequest request, VauEncryptionEnvelope vauEnvelope) {
    val req = this.unirest.post(getVauRequestUrl()).body(vauEnvelope.encrypted());
    this.setHeaders(req, request.headers());
    return req;
  }

  private void setHeaders(RequestBodyEntity req, List dynamicHeaders) {
    staticHeader.forEach(
        h -> {
          log.trace("Set static Header '{}' = '{}'", h.key(), h.value());
          req.header(h.key(), h.value());
        });

    dynamicHeaders.forEach(
        h -> {
          log.trace("Set dynamic Header '{}' = '{}'", h.key(), h.value());
          req.header(h.key(), h.value());
        });
  }

  private String getVauRequestUrl() {
    return fdBaseUrl + "/VAU/" + vauUserPseudonym;
  }

  public static VauClientBuilder forUrl(String url) {
    return new VauClientBuilder(url);
  }

  public static class VauClientBuilder {

    private final VauVersion vauVersion = VauVersion.V1;
    private final String url;
    private final List headers;
    private final VauObserverManager.VauObserverBuilder observerBuilder;
    private RawHttpCodec codec;
    private UnirestInstance unirest;

    private VauClientBuilder(String url) {
      this.url = url;
      this.observerBuilder = new VauObserverManager.VauObserverBuilder();
      this.headers = new LinkedList<>();

      // add default headers!
      this.headers.add(StandardHttpHeaderKey.CONTENT_TYPE.createHeader("application/octet-stream"));
      this.headers.add(StandardHttpHeaderKey.ACCEPT_CHARSET.createHeader("utf-8"));
      this.headers.add(StandardHttpHeaderKey.ACCEPT.createHeader("application/octet-stream"));
    }

    public VauClientBuilder usingApiKey(String apiKey) {
      this.headers.add(AuthHttpHeaderKey.X_API_KEY.createHeader(apiKey));
      return this;
    }

    public VauClientBuilder asUserAgent(String userAgent) {
      this.headers.add(StandardHttpHeaderKey.USER_AGENT.createHeader(userAgent));
      return this;
    }

    public VauClientBuilder withHeader(String key, String value) {
      return withHeader(new HttpHeader(key, value));
    }

    public VauClientBuilder withHeader(HttpHeader header) {
      this.headers.add(header);
      return this;
    }

    public VauClientBuilder withHeaders(List httpHeaders) {
      this.headers.addAll(httpHeaders);
      return this;
    }

    public VauClientBuilder withHttpCodec(RawHttpCodec codec) {
      this.codec = codec;
      return this;
    }

    public VauClientBuilder registerForRequests(HttpBRequestObserver ro) {
      this.observerBuilder.registerForRequests(ro);
      return this;
    }

    public VauClientBuilder registerForRequests(VauRequestObserver vro) {
      this.observerBuilder.registerForRequests(vro);
      return this;
    }

    public VauClientBuilder registerForResponses(HttpBResponseObserver ro) {
      this.observerBuilder.registerForResponses(ro);
      return this;
    }

    public VauClientBuilder registerForResponses(VauResponseObserver vro) {
      this.observerBuilder.registerForResponses(vro);
      return this;
    }

    public VauClientBuilder register(HttpBObserver rro) {
      return this.registerForRequests(rro).registerForResponses(rro);
    }

    public VauClientBuilder registerForVau(VauObserver vro) {
      return this.registerForRequests(vro).registerForResponses(vro);
    }

    public VauClientBuilder withoutTlsVerification() {
      val vauTrustManager = new EmptyTrustManager();
      return this.withTlsVerification(false, vauTrustManager);
    }

    public VauClientBuilder withTlsVerification(X509TrustManager trustManager) {
      return this.withTlsVerification(true, trustManager);
    }

    @SneakyThrows
    private VauClientBuilder withTlsVerification(boolean verifySsl, X509TrustManager trustManager) {
      val sslCtx = SSLContext.getInstance("TLS");
      sslCtx.init(null, new TrustManager[] {trustManager}, new SecureRandom());

      this.unirest = Unirest.spawnInstance();
      this.unirest.config().verifySsl(verifySsl).sslContext(sslCtx);
      return this;
    }

    @SneakyThrows
    public VauClient usingPublicKeyFromRemote() {
      val certUrl = format("{0}/VAUCertificate", this.url);
      val apiKey =
          this.headers.stream()
              .filter(sh -> sh.key().equalsIgnoreCase(AuthHttpHeaderKey.X_API_KEY.getKey()))
              .map(HttpHeader::value)
              .findFirst()
              .orElseThrow();
      val cert = VauCertificateDownload.downloadFrom(certUrl, apiKey);
      return this.usingPublicKey(cert);
    }

    public VauClient usingPublicKey(X509Certificate vauCertificate) {
      return this.usingPublicKey((ECPublicKey) vauCertificate.getPublicKey());
    }

    public VauClient usingPublicKey(@NonNull ECPublicKey publicKey) {
      if (this.unirest == null) {
        this.withoutTlsVerification();
      }

      if (this.codec == null) {
        this.withHttpCodec(RawHttpCodec.defaultCodec());
      }

      val vauProtocol = new VauProtocol(this.vauVersion, publicKey);
      return new VauClient(this, vauProtocol);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy