ee.ria.xroad.proxy.ProxyMain.java Source code

Java tutorial

Introduction

Here is the source code for ee.ria.xroad.proxy.ProxyMain.java

Source

/**
 * The MIT License
 * Copyright (c) 2015 Estonian Information System Authority (RIA), Population Register Centre (VRK)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package ee.ria.xroad.proxy;

import akka.actor.ActorSelection;
import akka.actor.ActorSystem;
import akka.pattern.Patterns;
import akka.util.Timeout;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import ee.ria.xroad.common.CommonMessages;
import ee.ria.xroad.common.DiagnosticsErrorCodes;
import ee.ria.xroad.common.DiagnosticsStatus;
import ee.ria.xroad.common.DiagnosticsUtils;
import ee.ria.xroad.common.PortNumbers;
import ee.ria.xroad.common.SystemProperties;
import ee.ria.xroad.common.SystemPropertiesLoader;
import ee.ria.xroad.common.conf.globalconf.GlobalConf;
import ee.ria.xroad.common.conf.serverconf.ServerConf;
import ee.ria.xroad.common.monitoring.MonitorAgent;
import ee.ria.xroad.common.signature.BatchSigner;
import ee.ria.xroad.common.util.AdminPort;
import ee.ria.xroad.common.util.JobManager;
import ee.ria.xroad.common.util.JsonUtils;
import ee.ria.xroad.common.util.StartStop;
import ee.ria.xroad.common.util.healthcheck.HealthCheckPort;
import ee.ria.xroad.proxy.addon.AddOn;
import ee.ria.xroad.proxy.clientproxy.ClientProxy;
import ee.ria.xroad.proxy.messagelog.MessageLog;
import ee.ria.xroad.proxy.opmonitoring.OpMonitoring;
import ee.ria.xroad.proxy.serverproxy.ServerProxy;
import ee.ria.xroad.proxy.util.CertHashBasedOcspResponder;
import ee.ria.xroad.signer.protocol.SignerClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import scala.concurrent.Await;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static ee.ria.xroad.common.SystemProperties.CONF_FILE_NODE;
import static ee.ria.xroad.common.SystemProperties.CONF_FILE_PROXY;
import static ee.ria.xroad.common.SystemProperties.CONF_FILE_SIGNER;

/**
 * Main program for the proxy server.
 */
@Slf4j
public final class ProxyMain {

    static {
        SystemPropertiesLoader.create().withCommonAndLocal().withAddOn().with(CONF_FILE_PROXY)
                .with(CONF_FILE_SIGNER).withLocalOptional(CONF_FILE_NODE).load();

        org.apache.xml.security.Init.init();
    }

    private static final int DIAGNOSTICS_CONNECTION_TIMEOUT_MS = 1200;
    private static final int DIAGNOSTICS_READ_TIMEOUT_MS = 15000; // 15 seconds

    private static final List<StartStop> SERVICES = new ArrayList<>();

    private static ActorSystem actorSystem;

    private static String version;
    private static ServiceLoader<AddOn> addOns = ServiceLoader.load(AddOn.class);

    private ProxyMain() {
    }

    /**
     * @return proxy version
     */
    public static String getVersion() {
        return version;
    }

    /**
     * Main program entry point.
     * @param args command-line arguments
     * @throws Exception in case of any errors
     */
    public static void main(String args[]) throws Exception {
        try {
            startup();
            loadConfigurations();
            startServices();
        } catch (Exception ex) {
            log.error("Proxy failed to start", ex);
            throw ex;
        } finally {
            shutdown();
        }
    }

    private static void startServices() throws Exception {
        log.trace("startServices()");

        createServices();

        for (StartStop service : SERVICES) {
            String name = service.getClass().getSimpleName();
            try {
                service.start();
                log.info("{} started", name);
            } catch (Exception e) {
                log.error(name + " failed to start", e);
                stopServices();
            }
        }

        for (StartStop service : SERVICES) {
            service.join();
        }

    }

    private static void stopServices() throws Exception {
        for (StartStop s : SERVICES) {
            log.debug("Stopping " + s.getClass().getSimpleName());
            s.stop();
            s.join();
        }
    }

    private static void startup() throws Exception {
        log.trace("startup()");

        actorSystem = ActorSystem.create("Proxy",
                ConfigFactory.load().getConfig("proxy").withFallback(ConfigFactory.load()).withValue(
                        "akka.remote.netty.tcp.port",
                        ConfigValueFactory.fromAnyRef(PortNumbers.PROXY_ACTORSYSTEM_PORT)));
        readProxyVersion();

        log.info("Starting proxy ({})...", getVersion());
    }

    private static void shutdown() throws Exception {
        log.trace("shutdown()");

        stopServices();
        actorSystem.shutdown();
    }

    private static void createServices() throws Exception {
        JobManager jobManager = new JobManager();

        MonitorAgent.init(actorSystem);
        SignerClient.init(actorSystem);
        BatchSigner.init(actorSystem);
        MessageLog.init(actorSystem, jobManager);
        OpMonitoring.init(actorSystem);

        for (AddOn addOn : addOns) {
            addOn.init(actorSystem);
        }

        SERVICES.add(jobManager);
        SERVICES.add(new ClientProxy());
        SERVICES.add(new ServerProxy());

        SERVICES.add(new CertHashBasedOcspResponder());

        SERVICES.add(createAdminPort());

        if (SystemProperties.isHealthCheckEnabled()) {
            SERVICES.add(new HealthCheckPort());
        }
    }

    private static void loadConfigurations() {
        log.trace("loadConfigurations()");

        try {
            GlobalConf.reload();
        } catch (Exception e) {
            log.error("Failed to load GlobalConf", e);
        }
    }

    private static AdminPort createAdminPort() throws Exception {
        AdminPort adminPort = new AdminPort(PortNumbers.ADMIN_PORT);

        adminPort.addShutdownHook(() -> {
            log.info("Proxy shutting down...");

            try {
                shutdown();
            } catch (Exception e) {
                log.error("Error while shutdown", e);
            }
        });

        /**
         * Diganostics for timestamping.
         * First check the connection to timestamp server. If OK, check the status of the previous timestamp request.
         * If the previous request has failed or connection cannot be made, DiagnosticsStatus tells the reason. If
         * LogManager is unavailable, uses the connection check to produce a more informative status.
         */
        adminPort.addHandler("/timestampstatus", new AdminPort.SynchronousCallback() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response) {
                log.info("/timestampstatus");

                Map<String, DiagnosticsStatus> result = checkConnectionToTimestampUrl();
                log.info("result {}", result);

                ActorSelection logManagerSelection = actorSystem.actorSelection("/user/LogManager");

                Timeout timeout = new Timeout(DIAGNOSTICS_CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS);
                try {
                    Map<String, DiagnosticsStatus> statusFromLogManager = (Map<String, DiagnosticsStatus>) Await
                            .result(Patterns.ask(logManagerSelection, CommonMessages.TIMESTAMP_STATUS, timeout),
                                    timeout.duration());

                    log.info("statusFromLogManager {}", statusFromLogManager.toString());

                    // Use the status either from simple connection check or from LogManager.
                    for (String key : result.keySet()) {
                        // If status exists in LogManager for given timestamp server, and it is successful or if
                        // simple connection check status is unsuccessful, use the status from LogManager
                        if (statusFromLogManager.get(key) != null
                                && (DiagnosticsErrorCodes.RETURN_SUCCESS == statusFromLogManager.get(key)
                                        .getReturnCode()
                                        && DiagnosticsErrorCodes.RETURN_SUCCESS == result.get(key).getReturnCode()
                                        || DiagnosticsErrorCodes.RETURN_SUCCESS != result.get(key).getReturnCode()
                                                && DiagnosticsErrorCodes.RETURN_SUCCESS != statusFromLogManager
                                                        .get(key).getReturnCode())) {
                            result.put(key, statusFromLogManager.get(key));

                            log.info("Using time stamping status from LogManager for url {} status: {}", key,
                                    statusFromLogManager.get(key));
                        } else if (statusFromLogManager.get(key) == null
                                && DiagnosticsErrorCodes.RETURN_SUCCESS == result.get(key).getReturnCode()) {
                            result.get(key)
                                    .setReturnCodeNow(DiagnosticsErrorCodes.ERROR_CODE_TIMESTAMP_UNINITIALIZED);
                        }
                    }
                } catch (Exception e) {
                    log.error("Unable to connect to LogManager, immediate timestamping status unavailable", e);
                    transmuteErrorCodes(result, DiagnosticsErrorCodes.RETURN_SUCCESS,
                            DiagnosticsErrorCodes.ERROR_CODE_LOGMANAGER_UNAVAILABLE);
                }

                try {
                    response.setCharacterEncoding("UTF8");
                    JsonUtils.getSerializer().toJson(result, response.getWriter());
                } catch (IOException e) {
                    log.error(
                            "Unable to write to provided response, delegated request handling failed, response may"
                                    + " be malformed",
                            e);
                }
            }
        });

        adminPort.addHandler("/maintenance", new AdminPort.SynchronousCallback() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response) {

                String result = "Invalid parameter 'targetState', request ignored";
                String param = request.getParameter("targetState");

                if (param != null && (param.equalsIgnoreCase("true") || param.equalsIgnoreCase("false"))) {
                    result = setHealthCheckMaintenanceMode(Boolean.valueOf(param));
                }
                try {
                    response.setCharacterEncoding("UTF8");
                    response.getWriter().println(result);
                } catch (IOException e) {
                    log.error(
                            "Unable to write to provided response, delegated request handling failed, response may"
                                    + " be malformed",
                            e);
                }
            }
        });

        return adminPort;
    }

    private static String setHealthCheckMaintenanceMode(boolean targetState) {
        return SERVICES.stream().filter(HealthCheckPort.class::isInstance).map(HealthCheckPort.class::cast)
                .findFirst().map(port -> port.setMaintenanceMode(targetState))
                .orElse("No HealthCheckPort found, maintenance mode not set");
    }

    private static void transmuteErrorCodes(Map<String, DiagnosticsStatus> map, int oldErrorCode,
            int newErrorCode) {
        map.forEach((key, value) -> {
            if (value != null && oldErrorCode == value.getReturnCode()) {
                value.setReturnCodeNow(newErrorCode);
            }
        });
    }

    private static Map<String, DiagnosticsStatus> checkConnectionToTimestampUrl() {
        Map<String, DiagnosticsStatus> statuses = new HashMap<>();

        for (String tspUrl : ServerConf.getTspUrl()) {
            try {
                URL url = new URL(tspUrl);

                log.info("Checking timestamp server status for url {}", url);

                HttpURLConnection con = (HttpURLConnection) url.openConnection();
                con.setConnectTimeout(DIAGNOSTICS_CONNECTION_TIMEOUT_MS);
                con.setReadTimeout(DIAGNOSTICS_READ_TIMEOUT_MS);
                con.setDoOutput(true);
                con.setDoInput(true);
                con.setRequestMethod("POST");
                con.setRequestProperty("Content-type", "application/timestamp-query");
                con.connect();

                log.info("Checking timestamp server con {}", con);

                if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
                    log.warn("Timestamp check received HTTP error: {} - {}. Might still be ok",
                            con.getResponseCode(), con.getResponseMessage());
                    statuses.put(tspUrl,
                            new DiagnosticsStatus(DiagnosticsErrorCodes.RETURN_SUCCESS, LocalTime.now(), tspUrl));
                } else {
                    statuses.put(tspUrl,
                            new DiagnosticsStatus(DiagnosticsErrorCodes.RETURN_SUCCESS, LocalTime.now(), tspUrl));
                }

            } catch (Exception e) {
                log.warn("Timestamp status check failed {}", e);

                statuses.put(tspUrl,
                        new DiagnosticsStatus(DiagnosticsUtils.getErrorCode(e), LocalTime.now(), tspUrl));
            }
        }
        return statuses;

    }

    private static void readProxyVersion() {
        try {
            String cmd;

            if (Files.exists(Paths.get("/etc/redhat-release"))) {
                cmd = "rpm -q --queryformat '%{VERSION}-%{RELEASE}' xroad-proxy";
            } else {
                cmd = "dpkg-query -f '${Version}' -W xroad-proxy";
            }

            Process p = Runtime.getRuntime().exec(cmd);
            p.waitFor();
            version = IOUtils.toString(p.getInputStream()).replace("'", "");

            if (StringUtils.isBlank(version)) {
                version = "unknown";

                log.warn("Unable to read proxy version: {}", IOUtils.toString(p.getErrorStream()));
            }
        } catch (Exception ex) {
            version = "unknown";
            log.warn("Unable to read proxy version", ex);
        }
    }
}