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

io.r2dbc.postgresql.PostgresqlConnectionConfiguration Maven / Gradle / Ivy

There is a newer version: 0.8.13.RELEASE
Show newest version
/*
 * Copyright 2017-2020 the original author or authors.
 *
 * 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
 *
 *      https://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 io.r2dbc.postgresql;

import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.r2dbc.postgresql.client.DefaultHostnameVerifier;
import io.r2dbc.postgresql.client.SSLConfig;
import io.r2dbc.postgresql.client.SSLMode;
import io.r2dbc.postgresql.codec.Codec;
import io.r2dbc.postgresql.codec.Codecs;
import io.r2dbc.postgresql.codec.Json;
import io.r2dbc.postgresql.extension.CodecRegistrar;
import io.r2dbc.postgresql.extension.Extension;
import io.r2dbc.postgresql.util.Assert;
import reactor.netty.resources.LoopResources;
import reactor.netty.tcp.SslProvider;
import reactor.util.annotation.Nullable;

import javax.net.ssl.HostnameVerifier;
import java.io.File;
import java.net.Socket;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;

import static io.r2dbc.postgresql.message.frontend.Execute.NO_LIMIT;
import static reactor.netty.tcp.SslProvider.DefaultConfigurationType.TCP;

/**
 * Connection configuration information for connecting to a PostgreSQL database.
 */
public final class PostgresqlConnectionConfiguration {

    /**
     * Default PostgreSQL port.
     */
    public static final int DEFAULT_PORT = 5432;

    private final String applicationName;

    private final boolean autodetectExtensions;

    private final Duration connectTimeout;

    private final String database;

    private final List extensions;

    private final ToIntFunction fetchSize;

    private final boolean forceBinary;

    private final String host;

    @Nullable
    private final LoopResources loopResources;

    private final Map options;

    private final CharSequence password;

    private final int port;

    private final boolean preferAttachedBuffers;

    private final int preparedStatementCacheQueries;

    private final String socket;

    private final SSLConfig sslConfig;

    private final boolean tcpKeepAlive;

    private final boolean tcpNoDelay;

    private final String username;

    private PostgresqlConnectionConfiguration(String applicationName, boolean autodetectExtensions, @Nullable Duration connectTimeout, @Nullable String database, List extensions,
                                              ToIntFunction fetchSize, boolean forceBinary, @Nullable String host, @Nullable LoopResources loopResources,
                                              @Nullable Map options, @Nullable CharSequence password, int port,boolean preferAttachedBuffers, int preparedStatementCacheQueries, @Nullable String schema,
                                              @Nullable String socket, SSLConfig sslConfig, boolean tcpKeepAlive, boolean tcpNoDelay, String username) {
        this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null");
        this.autodetectExtensions = autodetectExtensions;
        this.connectTimeout = connectTimeout;
        this.extensions = Assert.requireNonNull(extensions, "extensions must not be null");
        this.database = database;
        this.fetchSize = fetchSize;
        this.forceBinary = forceBinary;
        this.host = host;
        this.loopResources = loopResources;
        this.options = options == null ? new LinkedHashMap<>() : new LinkedHashMap<>(options);

        if (schema != null && !schema.isEmpty()) {
            this.options.put("search_path", schema);
        }

        this.password = password;
        this.port = port;
        this.preferAttachedBuffers = preferAttachedBuffers;
        this.preparedStatementCacheQueries = preparedStatementCacheQueries;
        this.socket = socket;
        this.sslConfig = sslConfig;
        this.tcpKeepAlive = tcpKeepAlive;
        this.tcpNoDelay = tcpNoDelay;
        this.username = Assert.requireNonNull(username, "username must not be null");
    }

    /**
     * Returns a new {@link Builder}.
     *
     * @return a new {@link Builder}
     */
    public static Builder builder() {
        return new Builder();
    }

    @Override
    public String toString() {
        return "PostgresqlConnectionConfiguration{" +
            "applicationName='" + this.applicationName + '\'' +
            ", autodetectExtensions='" + this.autodetectExtensions + '\'' +
            ", connectTimeout=" + this.connectTimeout +
            ", database='" + this.database + '\'' +
            ", extensions=" + this.extensions +
            ", fetchSize=" + this.fetchSize +
            ", forceBinary='" + this.forceBinary + '\'' +
            ", host='" + this.host + '\'' +
            ", loopResources='" + this.loopResources + '\'' +
            ", options='" + this.options + '\'' +
            ", password='" + obfuscate(this.password != null ? this.password.length() : 0) + '\'' +
            ", port=" + this.port +
            ", preferAttachedBuffers=" + this.preferAttachedBuffers +
            ", socket=" + this.socket +
            ", tcpKeepAlive=" + this.tcpKeepAlive +
            ", tcpNoDelay=" + this.tcpNoDelay +
            ", username='" + this.username + '\'' +
            '}';
    }

    String getApplicationName() {
        return this.applicationName;
    }

    @Nullable
    Duration getConnectTimeout() {
        return this.connectTimeout;
    }

    @Nullable
    String getDatabase() {
        return this.database;
    }

    List getExtensions() {
        return this.extensions;
    }

    ToIntFunction getFetchSize() {
        return this.fetchSize;
    }

    int getFetchSize(String sql) {
        return this.fetchSize.applyAsInt(sql);
    }

    @Nullable
    String getHost() {
        return this.host;
    }

    String getRequiredHost() {

        String host = getHost();

        if (host == null || host.isEmpty()) {
            throw new IllegalStateException("Connection is configured for socket connections and not for host usage");
        }

        return host;
    }

    @Nullable
    LoopResources getLoopResources() {
        return this.loopResources;
    }

    Map getOptions() {
        return Collections.unmodifiableMap(this.options);
    }

    @Nullable
    CharSequence getPassword() {
        return this.password;
    }

    int getPort() {
        return this.port;
    }

    boolean isPreferAttachedBuffers() {
        return this.preferAttachedBuffers;
    }

    int getPreparedStatementCacheQueries() {
        return this.preparedStatementCacheQueries;
    }

    @Nullable
    String getSocket() {
        return this.socket;
    }

    String getRequiredSocket() {

        String socket = getSocket();

        if (socket == null || socket.isEmpty()) {
            throw new IllegalStateException("Connection is configured to use host and port connections and not for socket usage");
        }

        return socket;
    }

    String getUsername() {
        return this.username;
    }

    boolean isAutodetectExtensions() {
        return this.autodetectExtensions;
    }

    boolean isForceBinary() {
        return this.forceBinary;
    }

    boolean isTcpKeepAlive() {
        return this.tcpKeepAlive;
    }

    boolean isTcpNoDelay() {
        return this.tcpNoDelay;
    }

    boolean isUseSocket() {
        return getSocket() != null;
    }

    SSLConfig getSslConfig() {
        return this.sslConfig;
    }

    private static String obfuscate(int length) {

        StringBuilder builder = new StringBuilder();

        for (int i = 0; i < length; i++) {
            builder.append("*");
        }

        return builder.toString();
    }

    /**
     * A builder for {@link PostgresqlConnectionConfiguration} instances.
     * 

* This class is not threadsafe */ public static final class Builder { private String applicationName = "r2dbc-postgresql"; private boolean autodetectExtensions = true; @Nullable private Duration connectTimeout; @Nullable private String database; private final List extensions = new ArrayList<>(); private ToIntFunction fetchSize = sql -> NO_LIMIT; private boolean forceBinary = false; @Nullable private String host; private Map options; @Nullable private CharSequence password; private int port = DEFAULT_PORT; private boolean preferAttachedBuffers = false; private int preparedStatementCacheQueries = -1; @Nullable private String schema; @Nullable private String socket; @Nullable private String sslCert = null; private HostnameVerifier sslHostnameVerifier = DefaultHostnameVerifier.INSTANCE; @Nullable private String sslKey = null; private SSLMode sslMode = SSLMode.DISABLE; @Nullable private CharSequence sslPassword = null; @Nullable private String sslRootCert = null; private Function sslContextBuilderCustomizer = Function.identity(); private boolean tcpKeepAlive = false; private boolean tcpNoDelay = true; @Nullable private LoopResources loopResources = null; @Nullable private String username; private Builder() { } /** * Configure the application name. Defaults to {@code postgresql-r2dbc}. * * @param applicationName the application name * @return this {@link Builder} * @throws IllegalArgumentException if {@code applicationName} is {@code null} */ public Builder applicationName(String applicationName) { this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null"); return this; } /** * Configures whether to use {@link ServiceLoader} to discover and register extensions. Defaults to true. * * @param autodetectExtensions to discover and register extensions * @return this {@link Builder} */ public Builder autodetectExtensions(boolean autodetectExtensions) { this.autodetectExtensions = autodetectExtensions; return this; } /** * Returns a configured {@link PostgresqlConnectionConfiguration}. * * @return a configured {@link PostgresqlConnectionConfiguration} */ public PostgresqlConnectionConfiguration build() { if (this.host == null && this.socket == null) { throw new IllegalArgumentException("host or socket must not be null"); } if (this.host != null && this.socket != null) { throw new IllegalArgumentException("Connection must be configured for either host/port or socket usage but not both"); } if (this.username == null) { throw new IllegalArgumentException("username must not be null"); } return new PostgresqlConnectionConfiguration(this.applicationName, this.autodetectExtensions, this.connectTimeout, this.database, this.extensions, this.fetchSize, this.forceBinary, this.host, this.loopResources, this.options, this.password, this.port, this.preferAttachedBuffers, this.preparedStatementCacheQueries, this.schema, this.socket, this.createSslConfig(), this.tcpKeepAlive, this.tcpNoDelay, this.username); } /** * Configures the connection timeout. Default unconfigured. * * @param connectTimeout the connection timeout * @return this {@link Builder} */ public Builder connectTimeout(@Nullable Duration connectTimeout) { this.connectTimeout = connectTimeout; return this; } /** * Registers a {@link CodecRegistrar} that can contribute extension {@link Codec}s. * Calling this method adds a {@link CodecRegistrar} and does not replace existing {@link Extension}s. * * @param codecRegistrar registrar to contribute codecs * @return this {@link Builder} */ public Builder codecRegistrar(CodecRegistrar codecRegistrar) { return extendWith(codecRegistrar); } /** * Configure the database. * * @param database the database * @return this {@link Builder} */ public Builder database(@Nullable String database) { this.database = database; return this; } /** * Enable SSL usage. This flag is also known as Use Encryption in other drivers. * * @return this {@link Builder} */ public Builder enableSsl() { return sslMode(SSLMode.VERIFY_FULL); } /** * Registers a {@link Extension} to extend driver functionality. * Calling this method adds a {@link Extension} and does not replace existing {@link Extension}s. * * @param extension extension to extend driver functionality * @return this {@link Builder} */ public Builder extendWith(Extension extension) { this.extensions.add(Assert.requireNonNull(extension, "extension must not be null")); return this; } /** * Set the default number of rows to return when fetching results from a query. If the value specified is zero, then the hint is ignored and queries request all rows when running a statement. * * @param fetchSize the number of rows to fetch * @return this {@code Builder} * @throws IllegalArgumentException if {@code fetchSize} is negative * @since 0.8.2 */ public Builder fetchSize(int fetchSize) { Assert.isTrue(fetchSize >= 0, "fetch size must be greater or equal zero"); return fetchSize(new FixedFetchSize(fetchSize)); } /** * Set a function that maps a SQL query to the number of rows to return when fetching results for that query. * * @param fetchSizeFunction a function that maps the number of rows to fetch * @return this {@code Builder} * @throws IllegalArgumentException if {@code fetchSizeFunction} is {@code null} * @since 0.8.2 */ public Builder fetchSize(ToIntFunction fetchSizeFunction) { Assert.requireNonNull(fetchSizeFunction, "fetch size function must be non null"); this.fetchSize = fetchSizeFunction; return this; } /** * Force binary results (Binary Transfer). Defaults to false. * * @param forceBinary whether to force binary transfer * @return this {@link Builder} */ public Builder forceBinary(boolean forceBinary) { this.forceBinary = forceBinary; return this; } /** * Configure the host. * * @param host the host * @return this {@link Builder} * @throws IllegalArgumentException if {@code host} is {@code null} */ public Builder host(String host) { this.host = Assert.requireNonNull(host, "host must not be null"); return this; } /** * Configure {@link LoopResources}. * * @param loopResources the {@link LoopResources} * @return this {@link Builder} * @since 0.8.5 */ public Builder loopResources(LoopResources loopResources) { this.loopResources = Assert.requireNonNull(loopResources, "loopResources must not be null"); return this; } /** * Configure connection initialization parameters. *

* These parameters are applied once after creating a new connection. This is useful for setting up client-specific * runtime parameters * like statement timeouts, time zones etc. * * @param options the options * @return this {@link Builder} * @throws IllegalArgumentException if {@code options} or any key or value of {@code options} is {@code null} */ public Builder options(Map options) { Assert.requireNonNull(options, "options map must not be null"); options.forEach((k, v) -> { Assert.requireNonNull(k, "option keys must not be null"); Assert.requireNonNull(v, "option values must not be null"); }); this.options = options; return this; } /** * Configure the password. * * @param password the password * @return this {@link Builder} */ public Builder password(@Nullable CharSequence password) { this.password = password; return this; } /** * Configure the port. Defaults to {@code 5432}. * * @param port the port * @return this {@link Builder} */ public Builder port(int port) { this.port = port; return this; } /** * Configure whether {@link Codecs codecs} should prefer attached data buffers. The default is {@code false}, meaning that codecs will copy data from the input buffer into a {@code byte[]} * or similar data structure that is enabled for garbage collection. Using attached buffers is more efficient but comes with the requirement that decoded values (such as {@link Json}) must * be consumed to release attached buffers to avoid memory leaks. * * @param preferAttachedBuffers the flag whether to prefer attached buffers * @return this {@link Builder} * @since 0.8.5 */ public Builder preferAttachedBuffers(boolean preferAttachedBuffers) { this.preferAttachedBuffers = preferAttachedBuffers; return this; } /** * Configure the preparedStatementCacheQueries. The default is {@code -1}, meaning there's no limit. The value of {@code 0} disables the cache. Any other value specifies the cache size. * * @param preparedStatementCacheQueries the preparedStatementCacheQueries * @return this {@link Builder} * @throws IllegalArgumentException if {@code username} is {@code null} * @since 0.8.1 */ public Builder preparedStatementCacheQueries(int preparedStatementCacheQueries) { this.preparedStatementCacheQueries = preparedStatementCacheQueries; return this; } /** * Configure the schema. * * @param schema the schema * @return this {@link Builder} */ public Builder schema(@Nullable String schema) { this.schema = schema; return this; } /** * Configure the unix domain socket to connect to. * * @param socket the socket path * @return this {@link Builder} * @throws IllegalArgumentException if {@code socket} is {@code null} */ public Builder socket(String socket) { this.socket = Assert.requireNonNull(socket, "host must not be null"); sslMode(SSLMode.DISABLE); return this; } /** * Configure a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL connection attempt to allow for just-in-time configuration updates. The {@link Function} gets * called with the prepared {@link SslContextBuilder} that has all configuration options applied. The customizer may return the same builder or return a new builder instance to be used to * build the SSL context. * * @param sslContextBuilderCustomizer customizer function * @return this {@link Builder} * @throws IllegalArgumentException if {@code sslContextBuilderCustomizer} is {@code null} */ public Builder sslContextBuilderCustomizer(Function sslContextBuilderCustomizer) { this.sslContextBuilderCustomizer = Assert.requireNonNull(sslContextBuilderCustomizer, "sslContextBuilderCustomizer must not be null"); return this; } /** * Configure ssl cert for client certificate authentication. * * @param sslCert an X.509 certificate chain file in PEM format * @return this {@link Builder} */ public Builder sslCert(String sslCert) { this.sslCert = Assert.requireFileExistsOrNull(sslCert, "sslCert must not be null and must exist"); return this; } /** * Configure ssl HostnameVerifier. * * @param sslHostnameVerifier {@link HostnameVerifier} * @return this {@link Builder} */ public Builder sslHostnameVerifier(HostnameVerifier sslHostnameVerifier) { this.sslHostnameVerifier = Assert.requireNonNull(sslHostnameVerifier, "sslHostnameVerifier must not be null"); return this; } /** * Configure ssl key for client certificate authentication. * * @param sslKey a PKCS#8 private key file in PEM format * @return this {@link Builder} */ public Builder sslKey(String sslKey) { this.sslKey = Assert.requireFileExistsOrNull(sslKey, "sslKey must not be null and must exist"); return this; } /** * Configure ssl mode. * * @param sslMode the SSL mode to use. * @return this {@link Builder} */ public Builder sslMode(SSLMode sslMode) { this.sslMode = Assert.requireNonNull(sslMode, "sslMode must be not be null"); return this; } /** * Configure ssl password. * * @param sslPassword the password of the sslKey, or null if it's not password-protected * @return this {@link Builder} */ public Builder sslPassword(@Nullable CharSequence sslPassword) { this.sslPassword = sslPassword; return this; } /** * Configure ssl root cert for server certificate validation. * * @param sslRootCert an X.509 certificate chain file in PEM format * @return this {@link Builder} */ public Builder sslRootCert(String sslRootCert) { this.sslRootCert = Assert.requireFileExistsOrNull(sslRootCert, "sslRootCert must not be null and must exist"); return this; } /** * Configure TCP KeepAlive. * * @param enabled whether to enable TCP KeepAlive * @return this {@link Builder} * @see Socket#setKeepAlive(boolean) * @since 0.8.4 */ public Builder tcpKeepAlive(boolean enabled) { this.tcpKeepAlive = enabled; return this; } /** * Configure TCP NoDelay. * * @param enabled whether to enable TCP NoDelay * @return this {@link Builder} * @see Socket#setTcpNoDelay(boolean) * @since 0.8.4 */ public Builder tcpNoDelay(boolean enabled) { this.tcpNoDelay = enabled; return this; } /** * Configure the username. * * @param username the username * @return this {@link Builder} * @throws IllegalArgumentException if {@code username} is {@code null} */ public Builder username(String username) { this.username = Assert.requireNonNull(username, "username must not be null"); return this; } @Override public String toString() { return "Builder{" + "applicationName='" + this.applicationName + '\'' + ", autodetectExtensions='" + this.autodetectExtensions + '\'' + ", connectTimeout='" + this.connectTimeout + '\'' + ", database='" + this.database + '\'' + ", extensions='" + this.extensions + '\'' + ", fetchSize='" + this.fetchSize + '\'' + ", forceBinary='" + this.forceBinary + '\'' + ", host='" + this.host + '\'' + ", loopResources='" + this.loopResources + '\'' + ", parameters='" + this.options + '\'' + ", password='" + obfuscate(this.password != null ? this.password.length() : 0) + '\'' + ", port=" + this.port + ", preparedStatementCacheQueries='" + this.preparedStatementCacheQueries + '\'' + ", schema='" + this.schema + '\'' + ", socket='" + this.socket + '\'' + ", sslContextBuilderCustomizer='" + this.sslContextBuilderCustomizer + '\'' + ", sslMode='" + this.sslMode + '\'' + ", sslRootCert='" + this.sslRootCert + '\'' + ", sslCert='" + this.sslCert + '\'' + ", sslKey='" + this.sslKey + '\'' + ", sslHostnameVerifier='" + this.sslHostnameVerifier + '\'' + ", tcpKeepAlive='" + this.tcpKeepAlive + '\'' + ", tcpNoDelay='" + this.tcpNoDelay + '\'' + ", username='" + this.username + '\'' + '}'; } private SSLConfig createSslConfig() { if (this.socket != null || this.sslMode == SSLMode.DISABLE) { return SSLConfig.disabled(); } HostnameVerifier hostnameVerifier = this.sslHostnameVerifier; return new SSLConfig(this.sslMode, createSslProvider(), hostnameVerifier); } private Supplier createSslProvider() { SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); if (this.sslMode.verifyCertificate()) { if (this.sslRootCert != null) { sslContextBuilder.trustManager(new File(this.sslRootCert)); } } else { sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); } String sslKey = this.sslKey; String sslCert = this.sslCert; // Emulate Libpq behavior // Determining the default file location String pathSeparator = System.getProperty("file.separator"); String defaultDir; if (System.getProperty("os.name").toLowerCase().contains("windows")) { // It is Windows defaultDir = System.getenv("APPDATA") + pathSeparator + "postgresql" + pathSeparator; } else { defaultDir = System.getProperty("user.home") + pathSeparator + ".postgresql" + pathSeparator; } if (sslCert == null) { String pathname = defaultDir + "postgresql.crt"; if (new File(pathname).exists()) { sslCert = pathname; } } if (sslKey == null) { String pathname = defaultDir + "postgresql.pk8"; if (new File(pathname).exists()) { sslKey = pathname; } } if (sslKey != null && sslCert != null) { String sslPassword = this.sslPassword == null ? null : this.sslPassword.toString(); sslContextBuilder.keyManager(new File(sslCert), new File(sslKey), sslPassword); } return () -> SslProvider.builder() .sslContext(this.sslContextBuilderCustomizer.apply(sslContextBuilder)) .defaultConfiguration(TCP) .build(); } } static class FixedFetchSize implements ToIntFunction { private final int fetchSize; public FixedFetchSize(int fetchSize) { this.fetchSize = fetchSize; } @Override public int applyAsInt(String value) { return this.fetchSize; } @Override public String toString() { return "" + this.fetchSize; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy