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

org.restheart.configuration.Configuration Maven / Gradle / Ivy

There is a newer version: 8.1.7
Show newest version
/*-
 * ========================LICENSE_START=================================
 * restheart-commons
 * %%
 * Copyright (C) 2019 - 2024 SoftInstigate
 * %%
 * 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.
 * =========================LICENSE_END==================================
 */
package org.restheart.configuration;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.jxpath.JXPathContext;
import org.restheart.configuration.Utils.RhOverride;
import static org.restheart.configuration.Utils.asMap;
import static org.restheart.configuration.Utils.findOrDefault;
import static org.restheart.configuration.Utils.overrides;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;

import com.google.common.collect.Maps;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.Strictness;
import com.mongodb.ConnectionString;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;

/**
 * Class that holds the configuration.
 *
 * @author Andrea Di Cesare {@literal }
 */
public class Configuration {

    private static final String LOG_PATTERN = "\t{} -> {}";

    private static final String MASK = "**********";

    private static final String LOCALHOST = "localhost";

    /**
     * the version is read from the JAR's MANIFEST.MF file, which is automatically
     * generated by the Maven build process
     */
    public static final String VERSION = Configuration.class.getPackage().getImplementationVersion() == null
            ? "unknown, not packaged"
            : Configuration.class.getPackage().getImplementationVersion();

    static final Logger LOGGER = LoggerFactory.getLogger(Configuration.class);

    public static final String DEFAULT_ROUTE = "0.0.0.0";

    /**
     * hold the path of the configuration file
     */
    private static Path PATH = null;

    private static final Listener DEFAULT_HTTP_LISTENER = new Listener(true, LOCALHOST, 8080);
    private static final TLSListener DEFAULT_HTTPS_LISTENER = new TLSListener(false, LOCALHOST, 4443, null, null, null);
    private static final Listener DEFAULT_AJP_LISTENER = new Listener(false, LOCALHOST, 8009);

    /**
     * undertow connetction options
     *
     * See
     * http://undertow.io/undertow-docs/undertow-docs-1.3.0/index.html#common-listener-optionshttp://undertow.io/undertow-docs/undertow-docs-1.3.0/index.html#common-listener-options
     */
    public static final String CONNECTION_OPTIONS_KEY = "connection-options";

    private final Listener httpListener;
    private final Listener ajpListener;
    private final TLSListener httpsListener;
    private final List proxies;
    private final List staticResources;
    private final CoreModule coreModule;
    private final Logging logging;
    private final Map connectionOptions;

    private final Map conf;

    /**
     * Creates a new instance of Configuration from the configuration file For any
     * missing property the default value is used.
     *
     * @param conf   the key-value configuration map
     * @param silent
     * @throws org.restheart.configuration.ConfigurationException
     */
    private Configuration(Map conf, final Path confFilePath, boolean silent)
            throws ConfigurationException {
        PATH = confFilePath;

        this.conf = conf;

        this.coreModule = CoreModule.build(conf, silent);

        if (findOrDefault(conf, Listener.HTTP_LISTENER_KEY, null, true) != null) {
            httpListener = new Listener(conf, Listener.HTTP_LISTENER_KEY, DEFAULT_HTTP_LISTENER, silent);
        } else {
            httpListener = DEFAULT_HTTP_LISTENER;
        }

        if (findOrDefault(conf, TLSListener.HTTPS_LISTENER_KEY, null, true) != null) {
            httpsListener = new TLSListener(conf, TLSListener.HTTPS_LISTENER_KEY, DEFAULT_HTTPS_LISTENER, silent);
        } else {
            httpsListener = DEFAULT_HTTPS_LISTENER;
        }

        if (findOrDefault(conf, Listener.AJP_LISTENER_KEY, null, true) != null) {
            ajpListener = new Listener(conf, Listener.AJP_LISTENER_KEY, DEFAULT_AJP_LISTENER, silent);
        } else {
            ajpListener = DEFAULT_AJP_LISTENER;
        }

        proxies = ProxiedResource.build(conf, silent);

        staticResources = StaticResource.build(conf, silent);

        logging = Logging.build(conf, silent);

        connectionOptions = asMap(conf, CONNECTION_OPTIONS_KEY, null, silent);
    }

    @Override
    public String toString() {
        var dumpOpts = new DumperOptions();
        dumpOpts.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
        dumpOpts.setPrettyFlow(true);
        dumpOpts.setIndent(2);
        dumpOpts.setCanonical(false);
        dumpOpts.setExplicitStart(true);

        var sw = new StringWriter();
        new Yaml(dumpOpts).dump(conf, sw);

        return sw.toString();
    }

    public  V getOrDefault(final String key, final V defaultValue) {
        return Utils.getOrDefault(this, key, defaultValue, true);
    }

    public Map toMap() {
        return Collections.unmodifiableMap(this.conf);
    }

    public CoreModule coreModule() {
        return coreModule;
    }

    /**
     * @return the proxies
     */
    public List getProxies() {
        return Collections.unmodifiableList(proxies);
    }

    /**
     * @return the staticResources
     */
    public List getStaticResources() {
        return Collections.unmodifiableList(staticResources);
    }

    /**
     * @return the httpListener
     */
    public Listener httpListener() {
        return httpListener;
    }

    /**
     * @return the httpsListener
     */
    public TLSListener httpsListener() {
        return httpsListener;
    }

    /**
     * @return the ajpListener
     */
    public Listener ajpListener() {
        return ajpListener;
    }

    /**
     * @return the logLevel
     */
    public Level getLogLevel() {
        var logbackConfigurationFile = System.getProperty("logback.configurationFile");
        if (logbackConfigurationFile != null && !logbackConfigurationFile.isEmpty()) {
            var loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
            var logger = loggerContext.getLogger("org.restheart");
            return logger.getLevel();
        }

        return logging.logLevel();
    }

    public Logging logging() {
        return logging;
    }

    /**
     * @return the connectionOptions
     */
    public Map getConnectionOptions() {
        return Collections.unmodifiableMap(connectionOptions);
    }

    /**
     *
     * @return the path of the configuration file
     */
    public static Path getPath() {
        return PATH;
    }

    public class Builder {

        private Builder() {
        }

        /**
         *
         * @param standaloneConfiguration
         * @param silent
         * @return the default configuration
         */
        public static Configuration build(boolean standaloneConfiguration, boolean silent) {
            return build(null, null, standaloneConfiguration, silent);
        }

        /**
         *
         * @param confFilePath
         * @param confOverridesFilePath
         * @param standaloneConfiguration
         * @param silent
         * @return return the configuration from confFile and propFile
         */
        public static Configuration build(Path confFilePath, Path confOverridesFilePath,
                boolean standaloneConfiguration, boolean silent) throws ConfigurationException {
            if (confFilePath == null) {
                var defaultConfFilePath = standaloneConfiguration ? "/restheart-default-config-no-mongodb.yml"
                        : "/restheart-default-config.yml";
                var stream = Configuration.class.getResourceAsStream(defaultConfFilePath);
                try (var confReader = new InputStreamReader(stream)) {
                    return build(confReader, null, confOverridesFilePath, silent);
                } catch (IOException ieo) {
                    throw new ConfigurationException("Error reading default configuration file", ieo);
                }
            } else {
                try (var confReader = new BufferedReader(new FileReader(confFilePath.toFile()))) {
                    return build(confReader, confFilePath, confOverridesFilePath, silent);
                } catch (FileNotFoundException ex) {
                    throw new ConfigurationException("Configuration file not found: " + confFilePath, ex, false);
                } catch (IOException ieo) {
                    throw new ConfigurationException("Error reading configuration file " + confFilePath, ieo);
                }
            }
        }

        /**
         *
         * @param confFile
         * @return return the configuration from confFile and propFile
         */
        private static Configuration build(Reader confReader, Path confFilePath, Path confOverridesFilePath,
                boolean silent) throws ConfigurationException {
            Map confMap = new Yaml(new SafeConstructor(new LoaderOptions())).load(confReader);

            if (confOverridesFilePath != null) {
                try {
                    String overrides;

                    if (confOverridesFilePath.toString().toLowerCase().endsWith(".yml")
                            || confOverridesFilePath.toString().toLowerCase().endsWith(".yaml")) {
                        // YML format
                        try {
                            overrides = fromYmlToRho(Files.newBufferedReader(confOverridesFilePath));
                        } catch (JsonParseException jpe) {
                            throw new ConfigurationException(
                                    "Wrong configuration override YML file: " + jpe.getLocalizedMessage(), jpe, false);
                        }
                    } else if (confOverridesFilePath.toString().toLowerCase().endsWith(".json")
                            || confOverridesFilePath.toString().toLowerCase().endsWith(".jsonc")) {
                        // JSON format
                        try {
                            overrides = fromJsonToRho(Files.newBufferedReader(confOverridesFilePath));
                        } catch (JsonParseException jpe) {
                            throw new ConfigurationException(
                                    "Wrong configuration override JSON file: " + jpe.getLocalizedMessage(), jpe, false);
                        }
                    } else if (confOverridesFilePath.toString().toLowerCase().endsWith(".conf")) {
                        // RHO format
                        overrides = Files.readAllLines(confOverridesFilePath).stream()
                                .filter(row -> !row.strip().startsWith("#")) // ingore comments lines
                                .collect(Collectors.joining());
                    } else {
                        throw new ConfigurationException(
                                "Configuration override file must have .json, .jsonc, .yml, .yaml or .conf extension: "
                                        + confOverridesFilePath);
                    }

                    if (!silent) {
                        LOGGER.info("Overriding configuration from file: {}", confOverridesFilePath);
                    }

                    confMap = overrideConfiguration(confMap, overrides(overrides, true, silent), silent);
                } catch (IOException ioe) {
                    throw new ConfigurationException("Configuration override file not found: " + confOverridesFilePath,
                            ioe, false);
                }
            }

            // overrides with RHO env var
            if (System.getenv().containsKey("RHO")) {
                if (!silent) {
                    LOGGER.info("Overriding configuration from environment variable RHO");
                }

                // overrides from RHO env var
                confMap = overrideConfiguration(confMap, overrides(System.getenv().get("RHO"), true, silent), silent);
            }

            return new Configuration(confMap, confFilePath, silent);
        }
    }

    // converst a JSON configuration override file into the RHO syntax
    // { "/logging/log-level": "INFO", "/core/name": "foo" } => /logging/log-level->"INFO";/core/name->"foo";
    private static String fromJsonToRho(Reader jsonReader) throws JsonParseException {
        var gson = new GsonBuilder().setStrictness(Strictness.LENIENT).create(); // setStrictness(Strictness.LENIENT) allows JSON with comments
        var _json = gson.fromJson(jsonReader, JsonObject.class);

        if (_json == null || !_json.isJsonObject()) {
            throw new JsonParseException("json is not an object");
        }

        var obj = _json.getAsJsonObject();

        return obj.entrySet().stream()
                .map(e -> e.getKey() + "->" + e.getValue().toString())
                .collect(Collectors.joining(";"));
    }

    // converst a YMM configuration override file into the RHO syntax
    // /logging/log-level: "INFO"
    // /core/name: "foo" => /logging/log-level->"INFO";/core/name->"foo";
    private static String fromYmlToRho(Reader yml) throws JsonParseException {
        Map _yml = new Yaml(new SafeConstructor(new LoaderOptions())).load(yml);

        if (_yml == null) {
            throw new JsonParseException("json is not an object");
        }

        final var gson = new GsonBuilder().serializeNulls().create();

        return _yml.entrySet().stream()
                .map(e -> e.getKey() + "->" + gson.toJson(e.getValue()))
                .collect(Collectors.joining(";"));
    }

    /**
     *
     * @param confMap
     * @return
     */
    private static Map overrideConfiguration(Map confMap, List overrides,
            final boolean silent) {
        var ctx = JXPathContext.newContext(confMap);
        ctx.setLenient(true);

        // this logs the overrides trying to mask sensite data
        // sensitive data is any key containins "password", "pwd" or "secret"
        // it also hides the password in MongoDB connection string
        // works also if the value is a Json object, checking the root keys (does not
        // hide nested properties)
        overrides.stream().forEachOrdered(o -> {
            if (!silent) {
                if (o.value() instanceof HashMap mapValue) {
                    var maskedValue = new HashMap();
                    mapValue.keySet().stream()
                            .filter(k -> k instanceof String)
                            .map(k -> (String) k)
                            .forEach(k -> {
                                if (k.contains("password") || k.contains("pwd") || k.contains("secret")
                                        || k.contains("key")) {
                                    maskedValue.put(k, MASK);
                                } else if (k.contains("connection-string")) {
                                    try {
                                        var svalue = mapValue.get(k).toString();
                                        var cs = new ConnectionString(svalue);
                                        var _pwd = cs.getPassword();
                                        if (_pwd != null) {
                                            var pwd = new String(_pwd);
                                            maskedValue.put(k, svalue.replaceFirst(pwd, MASK));
                                        }
                                    } catch (Throwable t) {
                                        maskedValue.put(k, mapValue);
                                    }
                                }
                            });
                    LOGGER.info(LOG_PATTERN, o.path(), maskedValue);
                } else if (o.path().contains("password") || o.path().contains("pwd") || o.path().contains("secret")
                        || o.path().contains("key")) {
                    LOGGER.info(LOG_PATTERN, o.path(), MASK);
                } else if (o.path().endsWith("connection-string") && o.value() instanceof String svalue) {
                    try {
                        var cs = new ConnectionString(svalue);
                        var _pwd = cs.getPassword();
                        if (_pwd != null) {
                            var pwd = new String(_pwd);
                            LOGGER.info(LOG_PATTERN, o.path(), svalue.replaceFirst(pwd, MASK));
                        }
                    } catch (Throwable t) {
                        LOGGER.info(LOG_PATTERN, o.path(), o.value());
                    }
                } else {
                    LOGGER.info(LOG_PATTERN, o.path(), o.value());
                }
            }

            if (!o.path().startsWith("/")) {
                LOGGER.error("Wrong configuration override {}, path must start with /", o.raw());
            } else {
                try {
                    createPathAndSetValue(ctx, o.path(), o.value());
                } catch (Throwable ise) {
                    LOGGER.error("Wrong configuration override {}, {}", o.raw(), ise.getMessage());
                }
            }
        });

        return confMap;
    }

    private static void createPathAndSetValue(JXPathContext ctx, String path, Object value) {
        createParents(ctx, path);
        ctx.createPathAndSetValue(path, value);
    }

    private static void createParents(JXPathContext ctx, String path) {
        var parentPath = path.substring(0, path.lastIndexOf("/"));

        if (!parentPath.equals("")) {
            createParents(ctx, parentPath);
        }

        var array = path.strip().endsWith("]");

        if (array) {
            // /a/b[2] -> /a/b
            var arrayPath = path.substring(0, path.lastIndexOf("["));
            if (ctx.getValue(arrayPath) == null) {
                ctx.createPathAndSetValue(arrayPath, new ArrayList<>());
            }
        } else {
            if (ctx.getValue(path) == null) {
                ctx.createPathAndSetValue(path, Maps.newLinkedHashMap());
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy