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

org.simplify4u.plugins.keyserver.PGPKeysServerClient Maven / Gradle / Ivy

/*
 * Copyright 2021 Slawomir Jaranowski
 *
 * 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 org.simplify4u.plugins.keyserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;

import com.google.common.io.ByteStreams;
import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.event.RetryEvent;
import io.vavr.CheckedRunnable;
import io.vavr.control.Try;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.ProxyAuthenticationStrategy;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import org.apache.maven.settings.Proxy;
import org.simplify4u.plugins.pgp.KeyId;
import org.simplify4u.plugins.utils.ExceptionUtils;

/**
 * Abstract base client for requesting keys from PGP key servers over HKP/HTTP and HKPS/HTTPS.
 */
@Slf4j
class PGPKeysServerClient {

    private static final List> IGNORE_EXCEPTION_FOR_RETRY =
            Arrays.asList(PGPKeyNotFound.class, UnknownHostException.class);

    private final KeyServerClientSettings keyServerClientSettings;

    private final URI keyserver;

    private final Supplier httpClientBuilderSupplier;

    /**
     * OnRetry hook interface.
     */
    @FunctionalInterface
    public interface OnRetryConsumer {

        /**
         * Call when retry operation occurs on a key server client.
         *
         * @param address               address used to retrieve key
         * @param numberOfRetryAttempts a number of try
         * @param waitInterval          wait time
         * @param lastThrowable         problem that cause to retry
         */
        void onRetry(InetAddress address, int numberOfRetryAttempts, Duration waitInterval, Throwable lastThrowable);
    }

    /**
     * Protected constructor for {@code PGPKeysServerClient}.
     *
     * @param keyserver                 The URI of the target key server.
     * @param keyServerClientSettings   The client configuration.
     * @param httpClientBuilderSupplier The http client builder.
     *
     * @see #getClient(String, KeyServerClientSettings)
     */
    protected PGPKeysServerClient(URI keyserver, KeyServerClientSettings keyServerClientSettings,
            Supplier httpClientBuilderSupplier) {
        this.keyserver = keyserver;
        this.keyServerClientSettings = keyServerClientSettings;
        this.httpClientBuilderSupplier = httpClientBuilderSupplier;
    }

    protected PGPKeysServerClient(URI keyserver, KeyServerClientSettings keyServerClientSettings) {
        this(keyserver, keyServerClientSettings, HttpClientBuilder::create);
    }

    /**
     * Create a PGP key server for a given URL.
     *
     * @param keyServer               The key server address / URL.
     * @param keyServerClientSettings The http client settings.
     *
     * @return The right PGP client for the given address.
     *
     * @throws IOException If some problem during client create.
     */
    static PGPKeysServerClient getClient(String keyServer, KeyServerClientSettings keyServerClientSettings)
            throws IOException {
        final URI uri = Try.of(() -> new URI(keyServer))
                .getOrElseThrow((Function) IOException::new);

        final String protocol = uri.getScheme().toLowerCase(Locale.ROOT);

        switch (protocol) {
            case "hkp":
            case "http":
                LOGGER.warn("hkp/http protocol are deprecated - please use hkps/https for key server");
                return new PGPKeysServerClientHttp(uri, keyServerClientSettings);

            case "hkps":
            case "https":
                return new PGPKeysServerClientHttps(uri, keyServerClientSettings);

            default:
                throw new IOException("Unsupported protocol: " + protocol);
        }
    }

    private static String getQueryStringForGetKey(KeyId keyID) {
        return String.format("op=get&options=mr&search=%s", keyID);
    }

    /**
     * Create URI for key download.
     *
     * @param keyID key ID
     *
     * @return URI with given key
     */
    URI getUriForGetKey(KeyId keyID) {
        return Try.of(() -> new URI(keyserver.getScheme(), keyserver.getUserInfo(),
                keyserver.getHost(), keyserver.getPort(),
                "/pks/lookup", getQueryStringForGetKey(keyID), null)).get();
    }

    private static String getQueryStringForShowKey(KeyId keyID) {
        return String.format("op=vindex&fingerprint=on&search=%s", keyID);
    }

    /**
     * Create URI for key lookup.
     *
     * @param keyID key ID
     *
     * @return URI with given key
     */
    URI getUriForShowKey(KeyId keyID) {
        return Try.of(() -> new URI(keyserver.getScheme(), keyserver.getUserInfo(),
                keyserver.getHost(), keyserver.getPort(),
                "/pks/lookup", getQueryStringForShowKey(keyID), null)).get();
    }

    /**
     * Requests the PGP key with the specified key ID from the server and copies it to the specified output stream.
     *
     * 

If the request fails due to connectivity issues or server load, the request will be * retried automatically according to the configuration of the provided retry handler. If the request still fails * after exhausting retries, the final exception will be re-thrown. * * @param keyId The ID of the key to request from the server. * @param outputStream The output stream to which the key will be written. * @param onRetryConsumer The consumer which will be call on retry occurs * * @throws IOException If the request fails, or the key cannot be written to the output stream. */ void copyKeyToOutputStream(KeyId keyId, OutputStream outputStream, OnRetryConsumer onRetryConsumer) throws IOException { final URI keyUri = getUriForGetKey(keyId); final HttpUriRequest request = new HttpGet(keyUri); final HttpRoutePlanner planer = keyServerClientSettings.getProxy() .map(PGPKeysServerClient::getNewProxyRoutePlanner) .orElseGet(RoundRobinRouterPlaner::new); RetryConfig config = RetryConfig.custom() .maxAttempts(keyServerClientSettings.getMaxRetries()) .intervalFunction(IntervalFunction.ofExponentialBackoff()) .retryOnException(PGPKeysServerClient::shouldRetryOnException) .build(); Retry retry = Retry.of("id", config); retry.getEventPublisher() .onRetry(event -> processOnRetry(event, event.getWaitInterval(), planer, onRetryConsumer)) .onError(event -> processOnRetry(event, Duration.ZERO, planer, onRetryConsumer)); CheckedRunnable checkedRunnable = Retry.decorateCheckedRunnable(retry, () -> { try (final CloseableHttpClient client = buildClient(planer); final CloseableHttpResponse response = client.execute(request)) { processKeyResponse(response, outputStream); } }); try { checkedRunnable.run(); } catch (PGPKeyNotFound e) { throw new PGPKeyNotFound("PGP server returned an error: HTTP/1.1 404 Not Found for: " + keyUri); } catch (Throwable e) { throw new IOException(ExceptionUtils.getMessage(e) + " for: " + keyUri, e); } } private static HttpRoutePlanner getNewProxyRoutePlanner(Proxy proxy) { HttpHost httpHost = new HttpHost(proxy.getHost(), proxy.getPort()); return new DefaultProxyRoutePlanner(httpHost); } private static boolean shouldRetryOnException(Throwable throwable) { Throwable aThrowable = throwable; while (aThrowable != null) { if (IGNORE_EXCEPTION_FOR_RETRY.contains(aThrowable.getClass())) { return false; } aThrowable = aThrowable.getCause(); } return true; } private void processOnRetry(RetryEvent event, Duration waitInterval, HttpRoutePlanner planer, OnRetryConsumer onRetryConsumer) { InetAddress targetAddress = null; if (planer instanceof RoundRobinRouterPlaner) { // inform planer about error on last roue HttpRoute httpRoute = ((RoundRobinRouterPlaner) planer).lastRouteCauseError(); targetAddress = Try.of(() -> httpRoute.getTargetHost().getAddress()).getOrElse((InetAddress) null); } else if (planer instanceof DefaultProxyRoutePlanner) { targetAddress = keyServerClientSettings.getProxy() .map(Proxy::getHost) .map(host -> Try.of(() -> InetAddress.getByName(host)).getOrNull()) .orElse(null); } // inform caller about retry if (onRetryConsumer != null) { onRetryConsumer.onRetry(targetAddress, event.getNumberOfRetryAttempts(), waitInterval, event.getLastThrowable()); } } // abstract methods to implemented in child class. /** * Verify that the provided response was successful, and then copy the response to the given output buffer. * *

If the response was not successful (e.g. not a "200 OK") status code, or the response * payload was empty, an {@link IOException} will be thrown. * * @param response A representation of the response from the server. * @param outputStream The stream to which the response data will be written. * * @throws IOException If the response was unsuccessful, did not contain any data, or could not be written * completely to the target output stream. */ private static void processKeyResponse(CloseableHttpResponse response, OutputStream outputStream) throws IOException { final StatusLine statusLine = response.getStatusLine(); if (statusLine.getStatusCode() == HttpStatus.SC_NOT_FOUND) { throw new PGPKeyNotFound(); } if (statusLine.getStatusCode() == HttpStatus.SC_OK) { final HttpEntity responseEntity = response.getEntity(); if (responseEntity == null) { throw new IOException("No response body returned."); } else { try (InputStream inputStream = responseEntity.getContent()) { ByteStreams.copy(inputStream, outputStream); } } } else { throw new IOException("PGP server returned an error: " + statusLine); } } /** * Build an HTTP client with the given router planer. * * @param planer The router planer for http client, used for load balancing * * @return The new HTTP client instance. */ CloseableHttpClient buildClient(HttpRoutePlanner planer) { final HttpClientBuilder clientBuilder = httpClientBuilderSupplier.get(); setupProxy(clientBuilder); applyTimeouts(clientBuilder); if (planer != null) { clientBuilder.setRoutePlanner(planer); } return clientBuilder.build(); } private void setupProxy(HttpClientBuilder clientBuilder) { Optional optProxy = keyServerClientSettings.getProxy(); optProxy.ifPresent(proxy -> { if (proxy.getUsername() != null && !proxy.getUsername().isEmpty() && proxy.getPassword() != null && !proxy.getPassword().isEmpty()) { AuthScope proxyAuthScope = new AuthScope(proxy.getHost(), proxy.getPort()); UsernamePasswordCredentials proxyAuthentication = new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword()); BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider(); basicCredentialsProvider.setCredentials(proxyAuthScope, proxyAuthentication); clientBuilder.setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE); clientBuilder.setDefaultCredentialsProvider(basicCredentialsProvider); } }); } /** * Set connect and read timeouts for an HTTP client that is being built. * * @param builder The client builder to which timeouts will be applied. */ private void applyTimeouts(final HttpClientBuilder builder) { final RequestConfig requestConfig = RequestConfig .custom() .setConnectionRequestTimeout(keyServerClientSettings.getConnectTimeout()) .setConnectTimeout(keyServerClientSettings.getConnectTimeout()) .setSocketTimeout(keyServerClientSettings.getReadTimeout()) .build(); builder.setDefaultRequestConfig(requestConfig); } @Override public String toString() { return "{" + keyserver + "}"; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy