text: // 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.
  
  #include "authentication/cram_md5/authenticatee.hpp"
  
  #include <stddef.h>   // For size_t needed by sasl.h.
  
  #include <sasl/sasl.h>
  
  #include <string>
  
  #include <process/defer.hpp>
  #include <process/dispatch.hpp>
  #include <process/once.hpp>
  #include <process/process.hpp>
  #include <process/protobuf.hpp>
  
  #include <stout/strings.hpp>
  
  #include "logging/logging.hpp"
  
  #include "messages/messages.hpp"
  
  // We need to disable the deprecation warnings as Apple has decided
  // to deprecate all of CyrusSASL's functions with OS 10.11
  // (see MESOS-3030). We are using GCC pragmas also for covering clang.
  #ifdef __APPLE__
  #pragma GCC diagnostic push
  #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
  #endif
  
  namespace mesos {
  namespace internal {
  namespace cram_md5 {
  
  using namespace process;
  using std::string;
  
  class CRAMMD5AuthenticateeProcess
    : public ProtobufProcess<CRAMMD5AuthenticateeProcess>
  {
  public:
    CRAMMD5AuthenticateeProcess(
        const Credential& _credential,
        const UPID& _client)
      : ProcessBase(ID::generate("crammd5-authenticatee")),
        credential(_credential),
        client(_client),
        status(READY),
        connection(nullptr)
    {
      const char* data = credential.secret().data();
      size_t length = credential.secret().length();
  
      // Need to allocate the secret via 'malloc' because SASL is
      // expecting the data appended to the end of the struct. *sigh*
      secret = (sasl_secret_t*) malloc(sizeof(sasl_secret_t) + length);
  
      CHECK(secret != nullptr) << "Failed to allocate memory for secret";
  
      memcpy(secret->data, data, length);
      secret->len = static_cast<unsigned long>(length);
    }
  
    virtual ~CRAMMD5AuthenticateeProcess()
    {
      if (connection != nullptr) {
        sasl_dispose(&connection);
      }
      free(secret);
    }
  
    virtual void finalize()
    {
      discarded(); // Fail the promise.
    }
  
    Future<bool> authenticate(const UPID& pid)
    {
      static Once* initialize = new Once();
      static bool initialized = false;
  
      if (!initialize->once()) {
        LOG(INFO) << "Initializing client SASL";
        int result = sasl_client_init(nullptr);
        if (result != SASL_OK) {
          status = ERROR;
          string error(sasl_errstring(result, nullptr, nullptr));
          promise.fail("Failed to initialize SASL: " + error);
          initialize->done();
          return promise.future();
        }
  
        initialized = true;
  
        initialize->done();
      }
  
      if (!initialized) {
        promise.fail("Failed to initialize SASL");
        return promise.future();
      }
  
      if (status != READY) {
        return promise.future();
      }
  
      LOG(INFO) << "Creating new client SASL connection";
  
      callbacks[0].id = SASL_CB_GETREALM;
      callbacks[0].proc = nullptr;
      callbacks[0].context = nullptr;
  
      callbacks[1].id = SASL_CB_USER;
      callbacks[1].proc = (int(*)()) &user;
      callbacks[1].context = (void*) credential.principal().c_str();
  
      // NOTE: Some SASL mechanisms do not allow/enable "proxying",
      // i.e., authorization. Therefore, some mechanisms send _only_ the
      // authorization name rather than both the user (authentication
      // name) and authorization name. Thus, for now, we assume
      // authorization is handled out of band. Consider the
      // SASL_NEED_PROXY flag if we want to reconsider this in the
      // future.
      callbacks[2].id = SASL_CB_AUTHNAME;
      callbacks[2].proc = (int(*)()) &user;
      callbacks[2].context = (void*) credential.principal().c_str();
  
      callbacks[3].id = SASL_CB_PASS;
      callbacks[3].proc = (int(*)()) &pass;
      callbacks[3].context = (void*) secret;
  
      callbacks[4].id = SASL_CB_LIST_END;
      callbacks[4].proc = nullptr;
      callbacks[4].context = nullptr;
  
      int result = sasl_client_new(
          "mesos",    // Registered name of service.
          nullptr,    // Server's FQDN.
          nullptr,    // IP Address information string.
          nullptr,    // IP Address information string.
          callbacks,  // Callbacks supported only for this connection.
          0,          // Security flags (security layers are enabled
                      // using security properties, separately).
          &connection);
  
      if (result != SASL_OK) {
        status = ERROR;
        string error(sasl_errstring(result, nullptr, nullptr));
        promise.fail("Failed to create client SASL connection: " + error);
        return promise.future();
      }
  
      AuthenticateMessage message;
      message.set_pid(client);
      send(pid, message);
  
      status = STARTING;
  
      // Stop authenticating if nobody cares.
      promise.future().onDiscard(defer(self(), &Self::discarded));
  
      return promise.future();
    }
  
  protected:
    virtual void initialize()
    {
      // Anticipate mechanisms and steps from the server.
      install<AuthenticationMechanismsMessage>(
          &CRAMMD5AuthenticateeProcess::mechanisms,
          &AuthenticationMechanismsMessage::mechanisms);
  
      install<AuthenticationStepMessage>(
          &CRAMMD5AuthenticateeProcess::step,
          &AuthenticationStepMessage::data);
  
      install<AuthenticationCompletedMessage>(
          &CRAMMD5AuthenticateeProcess::completed);
  
      install<AuthenticationFailedMessage>(
          &CRAMMD5AuthenticateeProcess::failed);
  
      install<AuthenticationErrorMessage>(
          &CRAMMD5AuthenticateeProcess::error,
          &AuthenticationErrorMessage::error);
    }
  
    void mechanisms(const std::vector<string>& mechanisms)
    {
      if (status != STARTING) {
        status = ERROR;
        promise.fail("Unexpected authentication 'mechanisms' received");
        return;
      }
  
      // TODO(benh): Store 'from' in order to ensure we only communicate
      // with the same Authenticator.
  
      LOG(INFO) << "Received SASL authentication mechanisms: "
                << strings::join(",", mechanisms);
  
      sasl_interact_t* interact = nullptr;
      const char* output = nullptr;
      unsigned length = 0;
      const char* mechanism = nullptr;
  
      int result = sasl_client_start(
          connection,
          strings::join(" ", mechanisms).c_str(),
          &interact,     // Set if an interaction is needed.
          &output,       // The output string (to send to server).
          &length,       // The length of the output string.
          &mechanism);   // The chosen mechanism.
  
      CHECK_NE(SASL_INTERACT, result)
        << "Not expecting an interaction (ID: " << interact->id << ")";
  
      if (result != SASL_OK && result != SASL_CONTINUE) {
        string error(sasl_errdetail(connection));
        status = ERROR;
        promise.fail("Failed to start the SASL client: " + error);
        return;
      }
  
      LOG(INFO) << "Attempting to authenticate with mechanism '"
                << mechanism << "'";
  
      AuthenticationStartMessage message;
      message.set_mechanism(mechanism);
      message.set_data(output, length);
  
      reply(message);
  
      status = STEPPING;
    }
  
    void step(const string& data)
    {
      if (status != STEPPING) {
        status = ERROR;
        promise.fail("Unexpected authentication 'step' received");
        return;
      }
  
      LOG(INFO) << "Received SASL authentication step";
  
      sasl_interact_t* interact = nullptr;
      const char* output = nullptr;
      unsigned length = 0;
  
      int result = sasl_client_step(
          connection,
          data.length() == 0 ? nullptr : data.data(),
          static_cast<unsigned>(data.length()),
          &interact,
          &output,
          &length);
  
      CHECK_NE(SASL_INTERACT, result)
        << "Not expecting an interaction (ID: " << interact->id << ")";
  
      if (result == SASL_OK || result == SASL_CONTINUE) {
        // We don't start the client with SASL_SUCCESS_DATA so we may
        // need to send one more "empty" message to the server.
        AuthenticationStepMessage message;
        if (output != nullptr && length > 0) {
          message.set_data(output, length);
        }
        reply(message);
      } else {
        status = ERROR;
        string error(sasl_errdetail(connection));
        promise.fail("Failed to perform authentication step: " + error);
      }
    }
  
    void completed()
    {
      if (status != STEPPING) {
        status = ERROR;
        promise.fail("Unexpected authentication 'completed' received");
        return;
      }
  
      LOG(INFO) << "Authentication success";
  
      status = COMPLETED;
      promise.set(true);
    }
  
    void failed()
    {
      status = FAILED;
      promise.set(false);
    }
  
    void error(const string& error)
    {
      status = ERROR;
      promise.fail("Authentication error: " + error);
    }
  
    void discarded()
    {
      status = DISCARDED;
      promise.fail("Authentication discarded");
    }
  
  private:
    static int user(
        void* context,
        int id,
        const char** result,
        unsigned* length)
    {
      CHECK(SASL_CB_USER == id || SASL_CB_AUTHNAME == id);
      *result = static_cast<const char*>(context);
      if (length != nullptr) {
        *length = static_cast<unsigned>(strlen(*result));
      }
      return SASL_OK;
    }
  
    static int pass(
        sasl_conn_t* connection,
        void* context,
        int id,
        sasl_secret_t** secret)
    {
      CHECK_EQ(SASL_CB_PASS, id);
      *secret = static_cast<sasl_secret_t*>(context);
      return SASL_OK;
    }
  
    const Credential credential;
  
    // PID of the client that needs to be authenticated.
    const UPID client;
  
    sasl_secret_t* secret;
  
    sasl_callback_t callbacks[5];
  
    enum
    {
      READY,
      STARTING,
      STEPPING,
      COMPLETED,
      FAILED,
      ERROR,
      DISCARDED
    } status;
  
    sasl_conn_t* connection;
  
    Promise<bool> promise;
  };
  
  
  Try<Authenticatee*> CRAMMD5Authenticatee::create()
  {
    return new CRAMMD5Authenticatee();
  }
  
  
  CRAMMD5Authenticatee::CRAMMD5Authenticatee() : process(nullptr) {}
  
  
  CRAMMD5Authenticatee::~CRAMMD5Authenticatee()
  {
    if (process != nullptr) {
      terminate(process);
      wait(process);
      delete process;
    }
  }
  
  
  Future<bool> CRAMMD5Authenticatee::authenticate(
    const UPID& pid,
    const UPID& client,
    const mesos::Credential& credential)
  {
    if (!credential.has_secret()) {
      LOG(WARNING) << "Authentication failed; secret needed by CRAM-MD5 "
                   << "authenticatee";
      return false;
    }
  
    CHECK(process == nullptr);
    process = new CRAMMD5AuthenticateeProcess(credential, client);
    spawn(process);
  
    return dispatch(
        process, &CRAMMD5AuthenticateeProcess::authenticate, pid);
  }
  
  } // namespace cram_md5 {
  } // namespace internal {
  } // namespace mesos {
  
  #ifdef __APPLE__
  #pragma GCC diagnostic pop
  #endif
  