// Copyright (C) 2009 The Android Open Source Project
//
// 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 com.google.gerrit.pgm.init;

import static com.google.gerrit.common.FileUtil.chmod;
import static com.google.gerrit.pgm.init.api.InitUtil.die;
import static com.google.gerrit.pgm.init.api.InitUtil.domainOf;
import static com.google.gerrit.pgm.init.api.InitUtil.isAnyAddress;
import static com.google.gerrit.pgm.init.api.InitUtil.toURI;

import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.pgm.init.api.Section;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.mail.SignedToken;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;

/** Initialize the {@code httpd} configuration section. */
@Singleton
class InitHttpd implements InitStep {
  private final ConsoleUI ui;
  private final SitePaths site;
  private final InitFlags flags;
  private final Section httpd;
  private final Section gerrit;

  @Inject
  InitHttpd(
      final ConsoleUI ui,
      final SitePaths site,
      final InitFlags flags,
      final Section.Factory sections) {
    this.ui = ui;
    this.site = site;
    this.flags = flags;
    this.httpd = sections.get("httpd", null);
    this.gerrit = sections.get("gerrit", null);
  }

  @Override
  public void run() throws IOException, InterruptedException {
    ui.header("HTTP Daemon");

    boolean anySsl = false;
    // If any listenUrls are present, validate whether it can be parsed as URL.
    String[] listenUrls = httpd.getList("listenUrl");
    for (String listenUrl : listenUrls) {
      if (listenUrl != null && !listenUrl.isEmpty()) {
        try {
          final URI u = toURI(listenUrl);
          if (u.getScheme().startsWith("https")) {
            anySsl = true;
          }
        } catch (URISyntaxException e) {
          System.err.println(
              String.format(
                  "warning: invalid httpd.listenUrl entry: '%s'. Gerrit may not be able to start.",
                  listenUrl));
        }
      }
    }

    if (listenUrls.length > 1) {
      // Because we will return early here, we will warn about steps otherwise being covered.
      if (!ui.isBatch()) {
        System.err.println(
            "Interactive configuration is not supported with multiple entries of "
                + "httpd.listenUrl.");
      }
      if (anySsl) {
        System.err.println(
            "Generating self-signed SSL certificates is not supported with multiple "
                + "entries of httpd.listenUrl.");
      }
      String canonicalWebUrlDefaultString = "";
      if (listenUrls[0] != null && !listenUrls[0].isEmpty()) {
        try {
          canonicalWebUrlDefaultString = new URI(listenUrls[0]).toString();
        } catch (URISyntaxException e) {
          // Should not happen, but log it anyway
          System.err.println(
              String.format("warning: invalid httpd.listenUrl entry: '%s'", listenUrls[0]));
        }
      }
      gerrit.string("Canonical URL", "canonicalWebUrl", canonicalWebUrlDefaultString);
      return;
    }

    boolean proxy = false;
    boolean ssl = false;
    String address = "*";
    int port = -1;
    String context = "/";

    if (listenUrls.length > 0 && listenUrls[0] != null && !listenUrls[0].isEmpty()) {
      try {
        final URI uri = toURI(listenUrls[0]);
        proxy = uri.getScheme().startsWith("proxy-");
        ssl = uri.getScheme().endsWith("https");
        address = isAnyAddress(new URI(listenUrls[0])) ? "*" : uri.getHost();
        port = uri.getPort();
        context = uri.getPath();
      } catch (URISyntaxException e) {
        System.err.println("warning: invalid httpd.listenUrl " + listenUrls[0]);
      }
    }

    proxy = ui.yesno(proxy, "Behind reverse proxy");

    if (proxy) {
      ssl = ui.yesno(ssl, "Proxy uses SSL (https://)");
      context = ui.readString(context, "Subdirectory on proxy server");
    } else {
      ssl = ui.yesno(ssl, "Use SSL (https://)");
      context = "/";
    }

    address = ui.readString(address, "Listen on address");

    if (port < 0) {
      if (proxy) {
        port = 8081;
      } else if (ssl) {
        port = 8443;
      } else {
        port = 8080;
      }
    }
    port = ui.readInt(port, "Listen on port");

    final StringBuilder urlbuf = new StringBuilder();
    urlbuf.append(proxy ? "proxy-" : "");
    urlbuf.append(ssl ? "https" : "http");
    urlbuf.append("://");
    urlbuf.append(address);
    if (0 <= port) {
      urlbuf.append(":");
      urlbuf.append(port);
    }
    urlbuf.append(context);

    URI uri;
    try {
      uri = toURI(urlbuf.toString());
      if (uri.getScheme().startsWith("proxy-")) {
        // If its a proxy URL, assume the reverse proxy is on our system
        // at the protocol standard ports (so omit the ports from the URL).
        //
        String s = uri.getScheme().substring("proxy-".length());
        uri = new URI(s + "://" + uri.getHost() + uri.getPath());
      }
    } catch (URISyntaxException e) {
      throw die("invalid httpd.listenUrl", e);
    }
    httpd.set("listenUrl", urlbuf.toString());
    gerrit.string("Canonical URL", "canonicalWebUrl", uri.toString());
    generateSslCertificate();
  }

  private void generateSslCertificate() throws IOException, InterruptedException {
    final String listenUrl = httpd.get("listenUrl");

    if (!listenUrl.startsWith("https://")) {
      // We aren't responsible for SSL processing.
      //
      return;
    }

    String hostname;
    try {
      String url = gerrit.get("canonicalWebUrl");
      if (url == null || url.isEmpty()) {
        url = listenUrl;
      }
      hostname = toURI(url).getHost();
    } catch (URISyntaxException e) {
      System.err.println("Invalid httpd.listenUrl, not checking certificate");
      return;
    }

    Path store = site.ssl_keystore;
    if (!ui.yesno(!Files.exists(store), "Create new self-signed SSL certificate")) {
      return;
    }

    String ssl_pass = flags.sec.get("http", null, "sslKeyPassword");
    if (ssl_pass == null || ssl_pass.isEmpty()) {
      ssl_pass = SignedToken.generateRandomKey();
      flags.sec.set("httpd", null, "sslKeyPassword", ssl_pass);
    }

    hostname = ui.readString(hostname, "Certificate server name");
    final String validity = ui.readString("365", "Certificate expires in (days)");

    final String dname = "CN=" + hostname + ",OU=Gerrit Code Review,O=" + domainOf(hostname);

    Path tmpdir = site.etc_dir.resolve("tmp.sslcertgen");
    try {
      Files.createDirectory(tmpdir);
    } catch (IOException e) {
      throw die("Cannot create directory " + tmpdir, e);
    }
    chmod(0600, tmpdir);

    Path tmpstore = tmpdir.resolve("keystore");
    Runtime.getRuntime()
        .exec(
            new String[] {
              "keytool", //
              "-keystore",
              tmpstore.toAbsolutePath().toString(), //
              "-storepass",
              ssl_pass, //
              "-genkeypair", //
              "-alias",
              hostname, //
              "-keyalg",
              "RSA", //
              "-validity",
              validity, //
              "-dname",
              dname, //
              "-keypass",
              ssl_pass, //
            })
        .waitFor();
    chmod(0600, tmpstore);

    try {
      Files.move(tmpstore, store);
    } catch (IOException e) {
      throw die("Cannot rename " + tmpstore + " to " + store, e);
    }
    try {
      Files.delete(tmpdir);
    } catch (IOException e) {
      throw die("Cannot delete " + tmpdir, e);
    }
  }
}
