/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.
 */
module thrift.internal.ssl;

import core.memory : GC;
import core.stdc.config;
import core.stdc.errno : errno;
import core.stdc.string : strerror;
import deimos.openssl.err;
import deimos.openssl.ssl;
import deimos.openssl.x509v3;
import std.array : empty, appender;
import std.conv : to;
import std.socket : Address;
import thrift.transport.ssl;

/**
 * Checks if the peer is authorized after the SSL handshake has been
 * completed on the given conncetion and throws an TSSLException if not.
 *
 * Params:
 *   ssl = The SSL connection to check.
 *   accessManager = The access manager to check the peer againts.
 *   peerAddress = The (IP) address of the peer.
 *   hostName = The host name of the peer.
 */
void authorize(SSL* ssl, TAccessManager accessManager,
  Address peerAddress, lazy string hostName
) {
  alias TAccessManager.Decision Decision;

  auto rc = SSL_get_verify_result(ssl);
  if (rc != X509_V_OK) {
    throw new TSSLException("SSL_get_verify_result(): " ~
      to!string(X509_verify_cert_error_string(rc)));
  }

  auto cert = SSL_get_peer_certificate(ssl);
  if (cert is null) {
    // Certificate is not present.
    if (SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) {
      throw new TSSLException(
        "Authorize: Required certificate not present.");
    }

    // If we don't have an access manager set, we don't intend to authorize
    // the client, so everything's fine.
    if (accessManager) {
      throw new TSSLException(
        "Authorize: Certificate required for authorization.");
    }
    return;
  }

  if (accessManager is null) {
    // No access manager set, can return immediately as the cert is valid
    // and all peers are authorized.
    X509_free(cert);
    return;
  }

  // both certificate and access manager are present
  auto decision = accessManager.verify(peerAddress);

  if (decision != Decision.SKIP) {
    X509_free(cert);
    if (decision != Decision.ALLOW) {
      throw new TSSLException("Authorize: Access denied based on remote IP.");
    }
    return;
  }

  // Check subjectAltName(s), if present.
  auto alternatives = cast(STACK_OF!(GENERAL_NAME)*)
    X509_get_ext_d2i(cert, NID_subject_alt_name, null, null);

  version(use_openssl_1_0_x) {
    enum _GEN_DNS = GENERAL_NAME.GEN_DNS;
    enum _GEN_IPADD = GENERAL_NAME.GEN_IPADD;
  } else version(use_openssl_1_1_x) {
    enum _GEN_DNS = GEN_DNS;
    enum _GEN_IPADD = GEN_IPADD;
  } else {
    static assert(false, `Must have version either use_openssl_1_0_x or use_openssl_1_1_x defined, e.g.
	"subConfigurations": {
		"apache-thrift": "use_openssl_1_0"
	}`);
  }

  if (alternatives != null) {
    auto count = sk_GENERAL_NAME_num(alternatives);
    for (int i = 0; decision == Decision.SKIP && i < count; i++) {
      auto name = sk_GENERAL_NAME_value(alternatives, i);
      if (name is null) {
        continue;
      }
      auto data = ASN1_STRING_data(name.d.ia5);
      auto length = ASN1_STRING_length(name.d.ia5);

      switch (name.type) {
        case _GEN_DNS:
          decision = accessManager.verify(hostName, cast(char[])data[0 .. length]);
          break;
        case _GEN_IPADD:
          decision = accessManager.verify(peerAddress, data[0 .. length]);
          break;
        default:
          // Do nothing.
      }
    }

    // DMD @@BUG@@: Empty template arguments parens should not be needed.
    sk_GENERAL_NAME_pop_free!()(alternatives, &GENERAL_NAME_free);
  }

  // If we are alredy done, return.
  if (decision != Decision.SKIP) {
    X509_free(cert);
    if (decision != Decision.ALLOW) {
      throw new TSSLException("Authorize: Access denied.");
    }
    return;
  }

  // Check commonName.
  auto name = X509_get_subject_name(cert);
  if (name !is null) {
    X509_NAME_ENTRY* entry;
    char* utf8;
    int last = -1;
    while (decision == Decision.SKIP) {
      last = X509_NAME_get_index_by_NID(name, NID_commonName, last);
      if (last == -1)
        break;
      entry = X509_NAME_get_entry(name, last);
      if (entry is null)
        continue;
      auto common = X509_NAME_ENTRY_get_data(entry);
      auto size = ASN1_STRING_to_UTF8(&utf8, common);
      decision = accessManager.verify(hostName, utf8[0 .. size]);
      CRYPTO_free(utf8);
    }
  }
  X509_free(cert);
  if (decision != Decision.ALLOW) {
    throw new TSSLException("Authorize: Could not authorize peer.");
  }
}

/*
 * OpenSSL error information used for storing D exceptions on the OpenSSL
 * error stack.
 */
enum ERR_LIB_D_EXCEPTION = ERR_LIB_USER;
enum ERR_F_D_EXCEPTION = 0; // function id - what to use here?
enum ERR_R_D_EXCEPTION = 1234; // 99 and above are reserved for applications
enum ERR_FILE_D_EXCEPTION = "d_exception";
enum ERR_LINE_D_EXCEPTION = 0;
enum ERR_FLAGS_D_EXCEPTION = 0;

/**
 * Returns an exception for the last.
 *
 * Params:
 *   location = An optional "location" to add to the error message (typically
 *     the last SSL API call).
 */
Exception getSSLException(string location = null, string clientFile = __FILE__,
  size_t clientLine = __LINE__
) {
  // We can return either an exception saved from D BIO code, or a "true"
  // OpenSSL error. Because there can possibly be more than one error on the
  // error stack, we have to fetch all of them, and pick the last, i.e. newest
  // one. We concatenate multiple successive OpenSSL error messages into a
  // single one, but always just return the last D expcetion.
  string message; // Probably better use an Appender here.
  bool hadMessage;
  Exception exception;

  void initMessage() {
    message.destroy();
    hadMessage = false;
    if (!location.empty) {
      message ~= location;
      message ~= ": ";
    }
  }
  initMessage();

  auto errn = errno;

  const(char)* file = void;
  int line = void;
  const(char)* data = void;
  int flags = void;
  c_ulong code = void;
  while ((code = ERR_get_error_line_data(&file, &line, &data, &flags)) != 0) {
    if (ERR_GET_REASON(code) == ERR_R_D_EXCEPTION) {
      initMessage();
      GC.removeRoot(cast(void*)data);
      exception = cast(Exception)data;
    } else {
      exception = null;

      if (hadMessage) {
        message ~= ", ";
      }

      auto reason = ERR_reason_error_string(code);
      if (reason) {
        message ~= "SSL error: " ~ to!string(reason);
      } else {
        message ~= "SSL error #" ~ to!string(code);
      }

      hadMessage = true;
    }
  }

  // If the last item from the stack was a D exception, throw it.
  if (exception) return exception;

  // We are dealing with an OpenSSL error that doesn't root in a D exception.
  if (!hadMessage) {
    // If we didn't get an actual error from the stack yet, try errno.
    string errnString;
    if (errn != 0) {
      errnString = to!string(strerror(errn));
    }
    if (errnString.empty) {
      message ~= "Unknown error";
    } else {
      message ~= errnString;
    }
  }

  message ~= ".";
  return new TSSLException(message, clientFile, clientLine);
}
