Server.java

/*
 * Copyright (C) 2020-2024 by Savoir-faire Linux
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package net.jami.jams.server;

import com.google.gson.Gson;

import javassist.ClassPool;

import lombok.extern.slf4j.Slf4j;

import net.jami.datastore.main.DataStore;
import net.jami.jams.common.annotations.ScopedServletAnnotationScanner;
import net.jami.jams.common.authentication.AuthenticationSourceType;
import net.jami.jams.common.authentication.local.LocalAuthSettings;
import net.jami.jams.common.authmodule.AuthenticationModule;
import net.jami.jams.common.cryptoengineapi.CertificateAuthority;
import net.jami.jams.common.jami.NameServer;
import net.jami.jams.common.serialization.adapters.GsonFactory;
import net.jami.jams.common.server.ServerSettings;
import net.jami.jams.common.utils.LibraryLoader;
import net.jami.jams.nameserver.LocalNameServer;
import net.jami.jams.nameserver.PublicNameServer;
import net.jami.jams.server.core.TomcatLauncher;
import net.jami.jams.server.licensing.LicenseService;
import net.jami.jams.server.startup.AuthModuleLoader;
import net.jami.jams.server.startup.CryptoEngineLoader;
import net.jami.jams.server.update.JAMSUpdater;
import net.jami.jams.server.update.UpdateInterface;

import org.bouncycastle.openssl.jcajce.JcaPEMWriter;

import java.awt.Desktop;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
// In order to make this "stoppable" to simply, I turned the server itself into
// a thread.
public class Server {

    public static final AtomicBoolean isInstalled = new AtomicBoolean(false);
    public static final AtomicBoolean activated = new AtomicBoolean(false);

    public static DataStore dataStore;
    // This one gets loaded via JAR, to make it more flexible.
    public static CertificateAuthority certificateAuthority;
    public static AuthenticationModule userAuthenticationModule;
    public static NameServer nameServer;
    public static TomcatLauncher tomcatLauncher = null;
    public static LicenseService licenseService = new LicenseService();
    public static final UpdateInterface updateInterface = new UpdateInterface();
    public static JAMSUpdater appUpdater = new JAMSUpdater(new AtomicBoolean(false));
    private static final Gson gson = GsonFactory.createGson();

    public static void main(String[] args) {
        // This is a fix to drop old cached stuff from the tomcat classloader.
        ClassPool.getDefault().clearImportedPackages();
        ScopedServletAnnotationScanner scanner = new ScopedServletAnnotationScanner();
        scanner.processClasses(
                new java.io.File(
                                Server.class
                                        .getProtectionDomain()
                                        .getCodeSource()
                                        .getLocation()
                                        .getPath())
                        .getName());
        switch (args.length) {
            case 1:
                tomcatLauncher = new TomcatLauncher(Integer.parseInt(args[0]));
                break;
            case 3:
                tomcatLauncher = new TomcatLauncher(Integer.parseInt(args[0]), args[1], args[2]);
                break;
            default:
                log.error("Incorrect number of start arguments provided!");
                System.exit(-1);
                break;
        }

        // Pre-load the libraries we should pre-load.
        LibraryLoader.loadlibs("libs", Server.class);
        // Step 1: Create the data store.
        dataStore = new DataStore("jdbc:derby:jams;create=true");

        // Create image folder if it doesn't exist.
        createImageFolder();

        File configJsonFile =
                new File(System.getProperty("user.dir") + File.separator + "config.json");
        isInstalled.set(configJsonFile.exists());
        log.info("Server is already installed: " + isInstalled.get());

        if (isInstalled.get()) {
            try {
                Reader reader = new FileReader(configJsonFile);
                loadConfig(reader);
                // This covers the case where the server is installed but the CA file is missing.
                createCAFile();
                log.info("All services are UP and ready for use...");
            } catch (Exception e) {
                log.error(
                        "Could not load configuration file or initialize some components - this is critical");
                System.exit(1);
            }
        } else {
            certificateAuthority = CryptoEngineLoader.loadCertificateAuthority(null, dataStore);
            userAuthenticationModule =
                    AuthModuleLoader.loadAuthenticationModule(dataStore, certificateAuthority);
            log.info("Started server with empty modules waiting for setup...");
        }
        // Now pop-up the GUI
        // Here we need to do a small spin-wait because keys may be slow to generate.
        try {
            startGUI();
            log.info("Server is now running!");
        } catch (Exception e) {
            log.error("Could not start GUI with error {}", e.getMessage());
        }
    }

    private static void createImageFolder() {
        File imagesFolder = new File("." + File.separator + "images");
        if (!imagesFolder.exists()) {
            boolean created = imagesFolder.mkdirs();
            if (created) {
                log.info("Created images folder");
            } else {
                log.error("Failed to create images folder");
                System.exit(-1);
            }
        }
    }

    private static void createCAFile() {
        if (!new File("CA.pem").exists()) {
            try {
                JcaPEMWriter pemWriter = new JcaPEMWriter(new FileWriter("CA.pem"));
                pemWriter.writeObject(certificateAuthority.getCA());
                pemWriter.close();
            } catch (IOException e) {
                log.error("Could not create CA file: " + e);
            }
        }
    }

    private static void loadConfig(Reader reader) {
        ServerSettings serverSettings = gson.fromJson(reader, ServerSettings.class);

        certificateAuthority =
                CryptoEngineLoader.loadCertificateAuthority(
                        serverSettings.getCaConfiguration(), dataStore);
        userAuthenticationModule =
                AuthModuleLoader.loadAuthenticationModule(dataStore, certificateAuthority);

        if (serverSettings.getLdapConfiguration() != null)
            userAuthenticationModule.attachAuthSource(
                    AuthenticationSourceType.LDAP, serverSettings.getLdapConfiguration());

        if (serverSettings.getActiveDirectoryConfiguration() != null) {
            userAuthenticationModule.attachAuthSource(
                    AuthenticationSourceType.AD, serverSettings.getActiveDirectoryConfiguration());
        }

        if (serverSettings.getLocalDirectoryConfiguration() != null) {
            LocalAuthSettings settings =
                    gson.fromJson(
                            serverSettings.getLocalDirectoryConfiguration(),
                            LocalAuthSettings.class);
            if (settings.getPublicNames())
                nameServer = new PublicNameServer(settings.getPublicNameServer());
            else
                nameServer =
                        new LocalNameServer(
                                dataStore,
                                userAuthenticationModule,
                                serverSettings.getServerPublicURI());
        } else
            nameServer =
                    new LocalNameServer(
                            dataStore,
                            userAuthenticationModule,
                            serverSettings.getServerPublicURI());

        licenseService.loadLicense();
    }

    public static void startGUI() throws Exception {
        boolean ready = false;
        while (!ready) {
            log.info("Waiting for authentication module to be ready...");
            if (userAuthenticationModule.getAuthModulePubKey() != null) {
                ready = true;
            }
            Thread.sleep(10);
        }
        log.info("Authentication module is ready!");
        if (Desktop.isDesktopSupported()
                && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
            Desktop.getDesktop()
                    .browse(
                            new URI(
                                    tomcatLauncher.getTomcat().getConnector().getScheme()
                                            + "://localhost:"
                                            + tomcatLauncher
                                                    .getTomcat()
                                                    .getService()
                                                    .findConnectors()[0]
                                                    .getPort()));
        } else {
            log.info("There is no graphical interface on this system - please connect remotely!");
        }
    }
}