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

com.io7m.certusine.etcd.internal.CSEtcdOutput Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2022 Mark Raynsford  https://www.io7m.com
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
 * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

package com.io7m.certusine.etcd.internal;

import com.io7m.certusine.api.CSCertificateOutputData;
import com.io7m.certusine.api.CSCertificateOutputType;
import com.io7m.certusine.etcd.internal.dto.CSEMessageType.CSEAuthenticateResponse;
import com.io7m.certusine.etcd.internal.dto.CSEMessageType.CSEError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

import static com.io7m.certusine.etcd.internal.dto.CSEMessageType.CEKV;
import static com.io7m.certusine.etcd.internal.dto.CSEMessageType.CERequestPut;
import static com.io7m.certusine.etcd.internal.dto.CSEMessageType.CSEAuthenticate;
import static com.io7m.certusine.etcd.internal.dto.CSEMessageType.CSETransaction;
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
import static java.net.http.HttpResponse.BodyHandlers.ofByteArray;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * An etcd certificate output.
 */

public final class CSEtcdOutput
  implements CSCertificateOutputType
{
  private static final Logger LOG =
    LoggerFactory.getLogger(CSEtcdOutput.class);
  private static final Pattern END_SLASHES =
    Pattern.compile("/+$");
  private final CSEtcdStrings strings;
  private final Optional credentials;
  private final String name;
  private final String endpoint;
  private final HttpClient client;
  private final CSEtcdMessages messages;
  private Optional token;

  /**
   * An etcd certificate output.
   *
   * @param inStrings     The strings
   * @param inCredentials The credentials
   * @param inName        The output name
   * @param inEndpoint    The endpoint base address
   */

  public CSEtcdOutput(
    final CSEtcdStrings inStrings,
    final String inName,
    final Optional inCredentials,
    final String inEndpoint)
  {
    this.strings =
      Objects.requireNonNull(inStrings, "inStrings");
    this.credentials =
      Objects.requireNonNull(inCredentials, "inCredentials");
    this.name =
      Objects.requireNonNull(inName, "name");
    this.endpoint =
      END_SLASHES.matcher(
          Objects.requireNonNull(inEndpoint, "inEndpoint"))
        .replaceAll("");

    this.client =
      HttpClient.newHttpClient();
    this.messages =
      new CSEtcdMessages();
    this.token =
      Optional.empty();
  }

  @Override
  public String type()
  {
    return "etcd";
  }

  @Override
  public String name()
  {
    return this.name;
  }

  @Override
  public void write(
    final CSCertificateOutputData outputData)
    throws IOException, InterruptedException
  {
    Objects.requireNonNull(outputData, "outputData");

    if (this.credentials.isPresent()) {
      this.authenticate(this.credentials.get());
    }

    this.sendData(outputData);
  }

  private void sendData(
    final CSCertificateOutputData outputData)
    throws IOException, InterruptedException
  {
    final var txnTarget =
      "%s/v3/kv/txn".formatted(this.endpoint);

    LOG.debug("transaction endpoint: {}", txnTarget);

    final var base64 =
      Base64.getUrlEncoder();

    final var namePub =
      "/certificates/%s/%s/public_key"
        .formatted(
          outputData.domainName(),
          outputData.name()
            .value());

    final var namePri =
      "/certificates/%s/%s/private_key"
        .formatted(
          outputData.domainName(),
          outputData.name()
            .value());

    final var nameCert =
      "/certificates/%s/%s/certificate"
        .formatted(
          outputData.domainName(),
          outputData.name()
            .value());

    final var nameCertFullChain =
      "/certificates/%s/%s/certificate_full_chain"
        .formatted(
          outputData.domainName(),
          outputData.name()
            .value());

    final var pubKV = new CEKV(
      base64.encodeToString(namePub.getBytes(UTF_8)),
      base64.encodeToString(outputData.pemEncodedPublicKey().getBytes(UTF_8))
    );

    final var priKV = new CEKV(
      base64.encodeToString(namePri.getBytes(UTF_8)),
      base64.encodeToString(outputData.pemEncodedPrivateKey().getBytes(UTF_8))
    );

    final var certKV = new CEKV(
      base64.encodeToString(nameCert.getBytes(UTF_8)),
      base64.encodeToString(outputData.pemEncodedCertificate().getBytes(UTF_8))
    );

    final var certChainKV = new CEKV(
      base64.encodeToString(nameCertFullChain.getBytes(UTF_8)),
      base64.encodeToString(outputData.pemEncodedFullChain().getBytes(UTF_8))
    );

    final var txn =
      new CSETransaction(
        List.of(),
        List.of(
          new CERequestPut(pubKV),
          new CERequestPut(priKV),
          new CERequestPut(certKV),
          new CERequestPut(certChainKV)
        )
      );

    final var builder =
      HttpRequest.newBuilder()
        .POST(ofByteArray(this.messages.serialize(txn)))
        .uri(URI.create(txnTarget));

    this.token.ifPresent(t -> builder.header("Authorization", t));

    final var request =
      builder.build();

    final var httpResponse =
      this.client.send(request, ofByteArray());
    final var data =
      httpResponse.body();
    final var message =
      this.messages.deserialize(data);
  }

  private void authenticate(
    final CSEtcdCredentials creds)
    throws IOException, InterruptedException
  {
    final var authTarget =
      "%s/v3/auth/authenticate".formatted(this.endpoint);

    LOG.debug("authentication endpoint: {}", authTarget);

    final var authMessage =
      new CSEAuthenticate(creds.user(), creds.password());
    final var request =
      HttpRequest.newBuilder()
        .POST(ofByteArray(this.messages.serialize(authMessage)))
        .uri(URI.create(authTarget))
        .build();

    final var httpResponse =
      this.client.send(request, ofByteArray());
    final var data =
      httpResponse.body();
    final var message =
      this.messages.deserialize(data);

    if (message instanceof CSEAuthenticateResponse authResponse) {
      this.token = Optional.of(authResponse.token());
    } else if (message instanceof CSEError error) {
      throw new IOException(error.message());
    } else {
      throw new IOException(
        this.strings.format("errorUnexpectedResponse", message)
      );
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy