/*
;    Project:       Open Vehicle Monitor System
;    Date:          14th March 2017
;
;    Changes:
;    1.0  Initial release
;
;    (C) 2011       Michael Stegen / Stegen Electronics
;    (C) 2011-2017  Mark Webb-Johnson
;    (C) 2011        Sonny Chen @ EPRO/DX
;
; 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.
*/

// We're using ESP_EARLY_LOG* (direct USB console output) for protocol debug logging.
// To enable protocol debug logging locally, uncomment:
// #define LOG_LOCAL_LEVEL ESP_LOG_VERBOSE

#include <sys/stat.h>
#define LWIP_POSIX_SOCKETS_IO_NAMES 0
#include <lwip/def.h>
#include <lwip/sockets.h>
#undef bind
#include "freertos/queue.h"
#include "esp_heap_caps.h"
#include "ovms_log.h"
#include "ovms_events.h"
#include "ovms_netmanager.h"
#include "ovms_config.h"
#include <wolfssl/wolfcrypt/memory.h>
#include <wolfssl/wolfcrypt/sha256.h>
#include <wolfssl/wolfcrypt/coding.h>
#include <wolfssl/wolfcrypt/rsa.h>
#include <wolfssl/wolfcrypt/sha.h>
#include <wolfssl/wolfcrypt/logging.h>
#include <wolfssh/ssh.h>
#include <wolfssh/log.h>
#include <wolfssh/internal.h>
#include "console_ssh.h"

static void wolfssh_logger(enum wolfSSH_LogLevel level, const char* const msg);
static void wolfssl_logger(int level, const char* const msg);
static void* wolfssl_malloc(size_t size);
static void wolfssl_free(void* ptr);
static void* wolfssl_realloc(void* ptr, size_t size);
static const char* const tag = "ssh";
static const char* const wolfssh_tag = "wolfssh";
static const char* const wolfssl_tag = "wolfssl";
static uint8_t CRLF[2] = { '\r', '\n' };
static const char newline = '\n';

//-----------------------------------------------------------------------------
//    Class OvmsSSH
//-----------------------------------------------------------------------------

OvmsSSH MySSH __attribute__ ((init_priority (8300)));

static void MongooseHandler(struct mg_connection *nc, int ev, void *p)
  {
  MySSH.EventHandler(nc, ev, p);
  }

void OvmsSSH::EventHandler(struct mg_connection *nc, int ev, void *p)
  {
  switch (ev)
    {
    case MG_EV_ACCEPT:
      {
      ESP_EARLY_LOGV(tag, "Event MG_EV_ACCEPT conn %p, data %p", nc, p);
      ConsoleSSH* child = new ConsoleSSH(this, nc);
      nc->user_data = child;
      break;
      }

    case MG_EV_POLL:
      {
      //ESP_EARLY_LOGV(tag, "Event MG_EV_ACCEPT conn %p, data %p", nc, p);
      ConsoleSSH* child = (ConsoleSSH*)nc->user_data;
      if (child)
        {
        child->Send();
        child->Poll(0);
        }
      if (!m_keyed)
        {
        std::string skey = MyConfig.GetParamValueBinary("ssh.server", "key", std::string());
        if (!skey.empty())
          {
          m_keyed = true;
          int ret = wolfSSH_CTX_UsePrivateKey_buffer(m_ctx, (const uint8_t*)skey.data(),
            skey.size(),  WOLFSSH_FORMAT_ASN1);
          if (ret < 0)
            ESP_LOGE(tag, "Couldn't use configured server key, error %d: %s", ret,
              GetErrorString(ret));
          else
            {
            std::string fp = MyConfig.GetParamValue("ssh.info", "fingerprint", "[not available]");
            ESP_LOGI(tag, "SSH server key installed with fingerprint %s", fp.c_str());
            }
          }
        }
      }
      break;

    case MG_EV_RECV:
      {
      ESP_EARLY_LOGV(tag, "Event MG_EV_RECV conn %p, data received %d", nc, *(int*)p);
      ConsoleSSH* child = (ConsoleSSH*)nc->user_data;
      child->Receive();
      }
      break;

    case MG_EV_SEND:
      {
      ESP_EARLY_LOGV(tag, "Event MG_EV_SEND conn %p, data %p", nc, p);
      ConsoleSSH* child = (ConsoleSSH*)nc->user_data;
      child->Sent();
      break;
      }

    case MG_EV_CLOSE:
      {
      ESP_EARLY_LOGV(tag, "Event MG_EV_CLOSE conn %p, data %p", nc, p);
      ConsoleSSH* child = (ConsoleSSH*)nc->user_data;
      if (child)
        delete child;
      }
      break;

    default:
      break;
    }
  }

OvmsSSH::OvmsSSH()
  {
  ESP_LOGI(tag, "Initialising SSH (8300)");
  m_keyed = false;
  m_ctx = NULL;

  using std::placeholders::_1;
  using std::placeholders::_2;
  MyEvents.RegisterEvent(tag,"network.mgr.init", std::bind(&OvmsSSH::NetManInit, this, _1, _2));
  MyEvents.RegisterEvent(tag,"network.mgr.stop", std::bind(&OvmsSSH::NetManStop, this, _1, _2));
  MyEvents.RegisterEvent(tag,"config.restore", std::bind(&OvmsSSH::ConfigRestore, this, _1, _2));
  MyConfig.RegisterParam("ssh.server", "SSH server private key store", true, false);
  MyConfig.RegisterParam("ssh.info", "SSH server information", false, true);
  MyConfig.RegisterParam("ssh.keys", "SSH public key store", true, true);
  }

void OvmsSSH::NetManInit(std::string event, void* data)
  {
  // Only initialise server for WIFI connections
  // TODO: Disabled as this introduces a network interface ordering issue. It
  //       seems that the correct way to do this is to always start the mongoose
  //       listener, but to filter incoming connections to check that the
  //       destination address is a Wifi interface address.
  // if (!(MyNetManager.m_connected_wifi || MyNetManager.m_wifi_ap))
  //   return;

  int ret;
  ESP_LOGI(tag, "Launching SSH Server");
  wolfSSH_SetLoggingCb(&wolfssh_logger);
  wolfSSH_Debugging_ON();
  wolfSSL_SetLoggingCb(&wolfssl_logger);
  wolfSSL_Debugging_ON();
  wolfSSL_SetAllocators(wolfssl_malloc, wolfssl_free, wolfssl_realloc);
  ret = wolfSSH_Init();
  if (ret != WS_SUCCESS)
    {
    ESP_LOGE(tag, "Couldn't initialize wolfSSH, error %d: %s", ret,
      GetErrorString(ret));
    return;
    }

  m_ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL);
  if (m_ctx == NULL)
    {
    ::printf("\nInsufficient memory to allocate SSH context\n");
    return;
    }
  wolfSSH_CTX_SetBanner(m_ctx, NULL);
  wolfSSH_SetUserAuth(m_ctx, Authenticate);
  std::string skey = MyConfig.GetParamValueBinary("ssh.server", "key", std::string());
  if (skey.empty())
    {
    ESP_LOGI(tag, "Generating SSH Server key, wait before attempting access.");
    new RSAKeyGenerator();
    }
  else
    {
    m_keyed = true;
    ret = wolfSSH_CTX_UsePrivateKey_buffer(m_ctx, (const uint8_t*)skey.data(),
      skey.size(),  WOLFSSH_FORMAT_ASN1);
    if (ret < 0)
      ESP_LOGE(tag, "Couldn't use configured server key, error %d: %s", ret,
        GetErrorString(ret));
    }

  struct mg_mgr* mgr = MyNetManager.GetMongooseMgr();
  mg_connection* nc = mg_bind(mgr, ":22", MongooseHandler);
  if (nc)
    nc->user_data = NULL;
  else
    ESP_LOGE(tag, "Launching SSH Server failed");
  }

void OvmsSSH::NetManStop(std::string event, void* data)
  {
  if (m_ctx)
    {
    ESP_LOGI(tag, "Stopping SSH Server");
    wolfSSH_CTX_free(m_ctx);
    if (wolfSSH_Cleanup() != WS_SUCCESS)
      ESP_LOGE(tag, "Couldn't clean up wolfSSH.");
    m_ctx = NULL;
    }
  }

void OvmsSSH::ConfigRestore(std::string event, void* data)
  {
  if (RSAKeyGenerator::KillInstance())
    {
    ESP_LOGW(tag, "RSAKeyGenerator killed by request");
    }
  }

int OvmsSSH::Authenticate(uint8_t type, WS_UserAuthData* data, void* ctx)
  {
  ConsoleSSH* cons = (ConsoleSSH*)ctx;
  if (type == WOLFSSH_USERAUTH_PASSWORD)
    {
    std::string user((const char*)data->username, data->usernameSz);
    std::string pw = MyConfig.GetParamValue("password", user, std::string());
    if (pw.empty())
      pw = MyConfig.GetParamValue("password", "module", std::string());
    if (pw.empty())
      {
      if (MyConfig.IsDefined("ssh.keys", ""))
        return WOLFSSH_USERAUTH_INVALID_PASSWORD; // Can't use null pw if have key
      else
        return WOLFSSH_USERAUTH_SUCCESS;  // No pw or key set means no authentication
      }
    if (pw.size() != data->sf.password.passwordSz ||
      memcmp(data->sf.password.password, pw.data(), pw.size()) != 0)
      return WOLFSSH_USERAUTH_INVALID_PASSWORD;
    }
  else if (type == WOLFSSH_USERAUTH_PUBLICKEY)
    {
    std::string user((const char*)data->username, data->usernameSz);
    std::string key = MyConfig.GetParamValue("ssh.keys", user, std::string());
    if (key.empty())
      return WOLFSSH_USERAUTH_INVALID_USER;
    byte der[560];
    uint32_t len = sizeof(der);
    if (Base64_Decode((const byte*)key.data(), key.size(), der, &len) != 0 ||
      len != data->sf.publicKey.publicKeySz ||
      memcmp(data->sf.publicKey.publicKey, der, len) != 0)
      return WOLFSSH_USERAUTH_INVALID_PUBLICKEY;
    }
  else
    return WOLFSSH_USERAUTH_INVALID_AUTHTYPE;
  cons->SetSecure(true); // Successful SSH authentication grants privileged access
  return WOLFSSH_USERAUTH_SUCCESS;
  }

//-----------------------------------------------------------------------------
//    Class ConsoleSSH
//-----------------------------------------------------------------------------

int RecvCallback(WOLFSSH* ssh, void* data, uint32_t size, void* ctx);
int SendCallback(WOLFSSH* ssh, void* data, uint32_t size, void* ctx);

ConsoleSSH::ConsoleSSH(OvmsSSH* server, struct mg_connection* nc)
  {
  m_server = server;
  m_connection = nc;
  m_queue = xQueueCreate(100, sizeof(Event));
  m_ssh = NULL;
  m_state = ACCEPT;
  m_drain = 0;
  m_sent = true;
  m_rekey = false;
  m_needDir = false;
  m_isDir = false;
  m_verbose = false;
  m_recursive = false;
  m_file = NULL;
  m_ssh = wolfSSH_new(m_server->ctx());
  if (m_ssh == NULL)
    {
    ::printf("Couldn't allocate SSH session data.\n");
    return;
    }
  wolfSSH_SetIORecv(m_server->ctx(), ::RecvCallback);
  wolfSSH_SetIOSend(m_server->ctx(), ::SendCallback);
  wolfSSH_SetIOReadCtx(m_ssh, this);
  wolfSSH_SetIOWriteCtx(m_ssh, m_connection);
  wolfSSH_SetUserAuthCtx(m_ssh, this);
  /* Use the session object for its own highwater callback ctx */
  wolfSSH_SetHighwaterCtx(m_ssh, (void*)m_ssh);
  wolfSSH_SetHighwater(m_ssh, DEFAULT_HIGHWATER_MARK);
  }

ConsoleSSH::~ConsoleSSH()
  {
  WOLFSSH* ssh = m_ssh;
  m_ssh = NULL;
  wolfSSH_free(ssh);
  vQueueDelete(m_queue);
  m_dirs.clear();
  }

// Handle MG_EV_RECV event.
void ConsoleSSH::Receive()
  {
  OvmsConsole::Event event;
  event.type = OvmsConsole::event_type_t::RECV;
  BaseType_t ret = xQueueSendToBack(m_queue, (void * )&event, (portTickType)(1000 / portTICK_PERIOD_MS));
  if (ret == pdPASS)
    {
    // Process the event we just queued in sequence with any queued logging.
    Poll(0);
    }
  else
    {
    ESP_EARLY_LOGE(tag, "Timeout queueing message in ConsoleSSH::Receive\n");
    mbuf *io = &m_connection->recv_mbuf;
    mbuf_remove(io, io->len);
    }
  }

// Handle MG_EV_POLL event.
void ConsoleSSH::Send()
  {
  if (m_state != SOURCE_SEND || !m_sent)
    {
    if (m_drain > 0 && m_connection->send_mbuf.len == 0)
      write("", 0);       // Wake up output after draining
    return;
    }
  int ret = 0;
  while (true)
    {
    if (m_size == 0)
      m_size = fread(m_buffer, sizeof(char), BUFFER_SIZE, m_file);
    if (m_size <= 0)
      break;
    m_sent = false;
    ret = wolfSSH_stream_send(m_ssh, (uint8_t*)m_buffer + m_index, m_size);
    if (ret < 0)
      break;
    m_size -= ret;
    if (m_size == 0)
      m_index = 0;
    else
      {
      m_index += ret;
      return;
      }
    if (!m_sent)
      return;
    }
  if (ret < 0)
    {
    // Would need to check ret != WS_WANT_WRITE here if mg_send is changed to
    // return EWOUDBLOCK
    ESP_EARLY_LOGE(tag, "Error %d in wolfSSH_stream_send: %s", ret, GetErrorString(ret));

    m_connection->flags |= MG_F_SEND_AND_CLOSE;
    m_state = CLOSING;
    }
  else if (m_size < 0)
    {
    ESP_EARLY_LOGE(tag, "Error %d reading file in source scp: %s", m_size, strerror(m_size));
    m_connection->flags |= MG_F_SEND_AND_CLOSE;
    m_state = CLOSING;
    }
  else  // EOF on fread()
    {
    ESP_EARLY_LOGD(tag, "Sending %s completed", m_path.c_str());
    m_state = SOURCE_RESPONSE;
    wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
    }
  fclose(m_file);
  m_file = NULL;
  }

// Handle MG_EV_SEND event.
void ConsoleSSH::Sent()
  {
  m_sent = true;
  }

int ConsoleSSH::GetResponse()
  {
  // Read the single binary 0 byte ACK or the firat byte of an error message or
  // protocol line.
  int rc = wolfSSH_stream_read(m_ssh, (uint8_t*)m_buffer, 1);
  if (rc < 1)
    return rc;
  if (m_state != SINK_LOOP || *m_buffer < ' ')
    ESP_EARLY_LOGD(tag, "response() received protocol ack byte %d", *m_buffer);
  if (*m_buffer != 0)
    {
    // Read the rest of the protocol line or error message, stopping on the
    // newline and leaving anything else queued.  We assume that it will all be
    // sent in one IP packet so we don't have to worry about WS_WANT_READ.
    char* p = m_buffer + 1;
    do
      rc = wolfSSH_stream_read(m_ssh, (uint8_t*)p, 1);
    while (rc == 1 && *p++ != '\n' && p < &m_buffer[BUFFER_SIZE-1]);
    *p-- = '\0';
    if (*p == '\n')
      *p = '\0';
    }
  return 1;
  }

// Handle RECV event from queue.
void ConsoleSSH::HandleDeviceEvent(void* pEvent)
  {
  struct stat sbuf;
  std::string msg;
  size_t filename = 0;
  Level level;
  int rc = 0;
  Event event = *(Event*)pEvent;
  if (event.type != RECV)
    {
    ESP_LOGE(tag, "Unknown event type %d in ConsoleSSH", event.type);
    return;
    }
  do
    {
    switch (m_state)
      {
      case ACCEPT:
        {
        rc = wolfSSH_accept(m_ssh);
        if (rc != WS_SUCCESS && (rc = wolfSSH_get_error(m_ssh)) != WS_SUCCESS)
          break;
        if (wolfSSH_GetSessionType(m_ssh) == WOLFSSH_SESSION_SHELL)
          {
          Initialize("SSH");
          m_state = SHELL;
          }
        else if (wolfSSH_GetSessionType(m_ssh) == WOLFSSH_SESSION_EXEC)
          {
          const char* cmdline = wolfSSH_GetSessionCommand(m_ssh);
          if (!cmdline)
            {
            rc = WS_BAD_USAGE;        // no command provided
            break;
            }
          ESP_LOGD(tag, "SSH command request: %s", cmdline);
          if (strncmp(cmdline, "scp -", 5) == 0)
            {
            bool from = false, to = false;
            int i;
            for (i = 5; ; ++i)
              {
              if (cmdline[i] == 'f') from = true;
              if (cmdline[i] == 't') to = true;
              if (cmdline[i] == 'd') m_needDir = true;
              if (cmdline[i] == 'v') m_verbose = true;
              if (cmdline[i] == 'r') m_recursive = true;
              if (cmdline[i] == ' ' && cmdline[++i] != '-') break;
              if (cmdline[i] == '\0') break;
              }
            m_path = &cmdline[i];
            if (from == to)
              rc = WS_BAD_USAGE;
            else if (from)
              m_state = SOURCE;
            else  // -t (to) case
              m_state = SINK;
            }
          else
            m_state = EXEC;
          }
        else  // Session did not request a 'shell' or 'exec' program
          rc = WS_UNIMPLEMENTED_E;
        break;
        }

      // Normal interactive shell session
      case SHELL:
        {
        rc = wolfSSH_stream_read(m_ssh, (uint8_t*)m_buffer, BUFFER_SIZE);
        if (rc > 0)
          {
          // Translate CR (Enter) from ssh client into \n for microrl
          for (int i = 0; i < rc; ++i)
            {
            if (m_buffer[i] == '\r')
              m_buffer[i] = '\n';
            }
          ProcessChars(m_buffer, rc);
          }
        break;
        }

      // 'exec' session with command to be executed and results returned
      case EXEC:
        {
        Initialize(NULL);
        const char* cmdline = wolfSSH_GetSessionCommand(m_ssh);
        ProcessChars(cmdline, strlen(cmdline));
        ProcessChar('\n');
        wolfSSH_stream_exit(m_ssh, 0); // XXX Need commands to return status
        m_state = CLOSING;
        break;
        }

      // SCP session pulling data from OVMS
      case SOURCE:
        {
        // Only expecting to read a single binary 0 byte
        rc = wolfSSH_stream_read(m_ssh, (uint8_t*)m_buffer, 1);
        if (rc < 1)
          break;
        ESP_EARLY_LOGD(tag, "SOURCE received protocol ack byte %d", *m_buffer);
        if (*m_buffer == 0)
          m_state = SOURCE_LOOP;
        else
          rc = WS_BAD_USAGE;  // client always sends 0 so this shouldn't happen
        break;
        }

      case SOURCE_LOOP:
        {
        ESP_LOGD(tag, "SCP 'from' file %s", m_path.c_str());
        msg.assign("\2scp: ").append(m_path).append(": ");
        if (MyConfig.ProtectedPath(m_path.c_str()))
          msg.append("protected path\n");
        else
          {
          while (m_path.size() > 0 && m_path.at(m_path.size()-1) == '/')
            m_path.resize(m_path.size()-1);          // Trim off a trailing '/'
          filename = m_path.find_last_of('/');
          if (filename == std::string::npos)
            filename = 0;
          else
            ++filename;
          int ret = 0;
          // Need to fake these because stat says "Invalid argument"
          if (m_path.compare("/store") == 0 || m_path.compare("/sd") == 0)
            sbuf.st_mode = S_IFDIR;  // Don't care about permissions
          else
            ret = stat(m_path.c_str(), &sbuf);
          if (ret < 0)
            msg.append(strerror(errno)).append("\n");
          else if (S_ISDIR(sbuf.st_mode))
            {
            if (!m_recursive)
              msg.append("received directory without -r\n");
            else if ((level.dir = opendir(m_path.c_str())) == NULL)
              msg.append(strerror(errno)).append("\n");
            else
              {
              level.size = m_path.size();
              m_dirs.push_front(level);
              msg.assign("D0755 0 ").append(m_path.substr(filename));
              ESP_LOGD(tag, "Source: %s", msg.c_str());
              msg.append("\n");
              wolfSSH_stream_send(m_ssh, (uint8_t*)msg.c_str(), msg.size());
              m_state = SOURCE_RESPONSE;
              break;
              }
            }
          else if (S_ISREG(sbuf.st_mode))
            {
            if ((m_file = fopen(m_path.c_str(), "r")) == NULL)
              msg.append(strerror(errno)).append("\n");
            else
              {
              char num[12];
              snprintf(num, sizeof(num), "%ld ", sbuf.st_size);
              msg.assign("C0644 ").append(num).append(m_path.substr(filename));
              ESP_LOGD(tag, "Source: %s", msg.c_str());
              msg.append("\n");
              wolfSSH_stream_send(m_ssh, (uint8_t*)msg.c_str(), msg.size());
              m_state = SOURCE_RESPONSE;
              break;
              }
            }
          else
            msg.append("not a regular file\n");
          }
        // Send the composed error message
        wolfSSH_stream_send(m_ssh, (uint8_t*)msg.c_str(), msg.size());
        m_state = SOURCE_RESPONSE;
        ESP_LOGI(tag, "%.*s", msg.size()-2, msg.substr(1,msg.size()-1).c_str());
        break;
        }

      case SOURCE_DIR:
        {
        level = m_dirs.front();
        m_path.resize(level.size);
        struct dirent* dp = readdir(level.dir);
        if (dp == NULL)
          {
          closedir(level.dir);
          m_dirs.pop_front();
          ESP_LOGD(tag, "Source: E");
          wolfSSH_stream_send(m_ssh, (uint8_t*)"E\n", 2);
          m_state = SOURCE_RESPONSE;
          break;
          }
        m_path.append("/").append(dp->d_name);
        m_state = SOURCE_LOOP;
        break;
        }

      case SOURCE_SEND:
      case SOURCE_RESPONSE:
        {
        rc = GetResponse();
        if (rc < 1)
          break;
	if (m_state == SOURCE_SEND)
	    ESP_LOGW(tag, "RECV not expected in SOURCE_SEND state");
        if (m_buffer[0] == '\1')
          {
          ESP_LOGW(tag, "Source: %s", &m_buffer[1]);
          if (m_file) // Abort the file we were going to send, if any
            {
            fclose(m_file);
            m_file = NULL;
            }
          }
        else if (m_buffer[0] == '\2')
          {
          ESP_LOGE(tag, "Source: %s", &m_buffer[1]);
          wolfSSH_stream_exit(m_ssh, 1);
          m_state = CLOSING;
          break;
          }
        if (m_file)
          {
          m_state = SOURCE_SEND;
          m_index = m_size = 0;
          Send();       // Send the first buffer, more when sent
          return;
          }
        if (!m_dirs.empty())
          {
          m_state = SOURCE_DIR;   // working on a directory
          break;
          }
        wolfSSH_stream_exit(m_ssh, 0);
        m_state = CLOSING;
        break;
        }

      // SCP session pushing data to OVMS
      case SINK:
        {
        msg.assign("\2scp: ").append(m_path).append(": ");
        while (m_path.size() > 1 && m_path.at(m_path.size()-1) == '/')
          {
          m_path.resize(m_path.size()-1);          // Trim off a trailing '/'
          m_needDir = true;
          }
        level.size = m_path.size();
        m_dirs.push_front(level);
        filename = std::string::npos;
        int ret = 0;
        // Need to fake these because stat says "Invalid argument"
        if (m_path.compare("/store") == 0 || m_path.compare("/sd") == 0)
          sbuf.st_mode = S_IFDIR;  // Don't care about permissions
        else
          ret = stat(m_path.c_str(), &sbuf);
        if (ret < 0 && errno == ENOENT && !m_needDir &&
          (filename = m_path.find_last_of('/')) != std::string::npos)
          {
          // Check path dirname for case where we're creating a new file
          int retd = stat(m_path.substr(0, filename).c_str(), &sbuf);
          if (retd == 0)
            ret = retd;
          }
        if (ret < 0)
          msg.append(strerror(errno)).append("\n");
        else if (S_ISDIR(sbuf.st_mode) || S_ISREG(sbuf.st_mode))
          {
          m_isDir = S_ISDIR(sbuf.st_mode) && filename == std::string::npos;
          if (m_isDir || !m_needDir)
            {
            m_state = SINK_LOOP;
            wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
            break;
            }
          msg.append(strerror(ENOTDIR)).append("\n");
          }
        else
          msg.append("not a regular file\n");  // Probably can't hit this
        wolfSSH_stream_send(m_ssh, (uint8_t*)msg.c_str(), msg.size());
        ESP_LOGI(tag, "%.*s", msg.size()-2, msg.substr(1,msg.size()-1).c_str());
        wolfSSH_stream_exit(m_ssh, 0);
        m_state = CLOSING;
        break;
        }

      case SINK_LOOP:
        {
        // Read the protocol line or error message.
        rc = GetResponse();
        if (rc < 1)
          break;
        if (*m_buffer == '\1')
          {
          ESP_LOGW(tag, "Sink: %s", &m_buffer[1]);
          break;  // Continue with more protocol lines
          }
        else if (*m_buffer == '\2')
          {
          ESP_LOGE(tag, "Sink: %s", &m_buffer[1]);
          wolfSSH_stream_exit(m_ssh, 1);
          m_state = CLOSING;
          break;
          }
        ESP_LOGD(tag, "Sink: %s", m_buffer);
        msg.assign("\2scp: ");
        if (*m_buffer == 'T')                         // Times are ignored
          {
          wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
          break;
          }
        else if (*m_buffer == 'E')
          {
          level = m_dirs.front();
          m_path.resize(level.size);
          m_dirs.pop_front();
          if (!m_dirs.empty())
            {
            wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
            break;
            }
          msg.append("protocol error: unbalanced D-E pair\n");
          }
        else if (*m_buffer == 'D' || *m_buffer == 'C')
          {
          char* end = m_buffer;
          if (m_buffer[1] != '0' || m_buffer[2] < '0' || m_buffer[2] > '7' ||
            m_buffer[3] < '0' || m_buffer[3] > '7' || m_buffer[4] < '0' || m_buffer[4] > '7' ||
            m_buffer[5] != ' ')
            msg.append("bad mode ").append(&m_buffer[1], 4).append("\n");
          else if ((m_size = strtoul(&m_buffer[6], &end, 10)) < 0 || m_size > 10485760 || *end++ != ' ')
            msg.append("size unacceptable: ").append(&m_buffer[6], end-&m_buffer[6]).append("\n");
          else if ((strchr(end, '/') != NULL) || (strcmp(end, "..") == 0))
            msg.append("unexpected filename: ").append(end).append("\n");
          else
            {
            level = m_dirs.front();
            m_path.resize(level.size);
            if (m_isDir)
              {
              if (m_path.compare("/") != 0)  // Probably impossible to target "/" on OVMS
                m_path.append("/");
              m_path.append(end);
              }
            bool exists = (stat(m_path.c_str(), &sbuf) == 0);
            msg.append(m_path).append(": ");
            if (MyConfig.ProtectedPath(m_path.c_str()))
              msg.append("protected path\n");
            else if (*m_buffer == 'D')
              {
              if (!m_recursive)
                msg.append("received directory without -r\n");
              else
                {
                level.size = m_path.size();
                m_dirs.push_front(level);
                if (exists)
                  {
                  if (!S_ISDIR(sbuf.st_mode))
                    msg.append(strerror(ENOTDIR)).append("\n");
                  else
                    {
                    wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
                    break;
                    }
                  }
                else
                  {
                  ESP_EARLY_LOGD(tag, "mkdir(\"%s\", 0777)", m_path.c_str());
                  if (mkdir(m_path.c_str(), 0777) < 0)
                    msg.append("mkdir: ").append(strerror(errno)).append("\n");
                  else
                    {
                    wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
                    break;
                    }
                  }
                }
              }
            else // 'C'
              {
              if (exists && !S_ISREG(sbuf.st_mode))
                msg.append("not a regular file\n");
              else
                {
                ESP_EARLY_LOGD(tag, "fopen(\"%s\", \"w\")", m_path.c_str());
                if ((m_file = fopen(m_path.c_str(), "w")) == NULL)
                  msg.append("fopen: ").append(strerror(errno)).append("\n");
                else
                  {
                  m_state = SINK_RECEIVE;
                  wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
                  break;
                  }
                }
              }
            }
          }
        else
          msg.append("protocol error: ").append(m_buffer).append("\n");
        wolfSSH_stream_send(m_ssh, (uint8_t*)msg.c_str(), msg.size());
        ESP_LOGI(tag, "%.*s", msg.size()-2, msg.substr(1,msg.size()-1).c_str());
        wolfSSH_stream_exit(m_ssh, 1);
        break;
        }

      case SINK_RECEIVE:
        {
        int size = BUFFER_SIZE;
        if (m_size < size)
          size = m_size;
        rc = wolfSSH_stream_read(m_ssh, (uint8_t*)m_buffer, size);
        if (rc < 0)
          break;       // Probably WS_WANT_READ, loop end checks it
        size = fwrite(m_buffer, sizeof(uint8_t), rc, m_file);
        if (size < rc)
          ESP_LOGE(tag, "Write error on %s: %s", m_path.c_str(), strerror(errno));
        m_size -= rc;
        if (m_size == 0)
          {
          fclose(m_file);
          m_file = NULL;
          m_state = SINK_RESPONSE;
          wolfSSH_stream_send(m_ssh, (uint8_t*)"", 1);
          }
        break;
        }

      case SINK_RESPONSE:
        {
        rc = GetResponse();
        if (rc < 1)
          break;
        if (*m_buffer == '\1')       // Warning, so we continue
          ESP_LOGW(tag, "Sink: %s", &m_buffer[1]);
        else if (*m_buffer == '\2')  // Error, so we exit
          {
          ESP_LOGE(tag, "Sink: %s", &m_buffer[1]);
          wolfSSH_stream_exit(m_ssh, 1);
          m_state = CLOSING;
          break;
          }
        m_state = SINK_LOOP;
        break;
        }

      case CLOSING:
        ESP_EARLY_LOGD(tag, "Reached CLOSING");
        // Try to read to let SSH process packets, but ignore anything delivered
        rc = wolfSSH_stream_read(m_ssh, (uint8_t*)m_buffer, BUFFER_SIZE);
        break;
      }
    } while (rc >= 0);

  if (rc == WS_ERROR)
    rc = wolfSSH_get_error(m_ssh);
  if (rc == WS_WANT_READ || rc == WS_BAD_ARGUMENT)  // Latter => channel closed
    return;
  if (rc == WS_EOF)
    {
    wolfSSH_stream_exit(m_ssh, 0);
    m_state = CLOSING;
    return;
    }
  ESP_LOGE(tag, "Error %d in reception: %s", rc, GetErrorString(rc));
  m_connection->flags |= MG_F_SEND_AND_CLOSE;
  }

// This is called to shut down the SSH connection when the "exit" command is input.
void ConsoleSSH::Exit()
  {
  printf("logout\n");
  wolfSSH_stream_exit(m_ssh, 0);
  }

int ConsoleSSH::puts(const char* s)
  {
  if (!m_ssh)
    return -1;
  write(s, strlen(s));
  write(&newline, 1);
  return 0;
  }

int ConsoleSSH::printf(const char* fmt, ...)
  {
  if (!m_ssh)
    return 0;
  char *buffer = NULL;
  va_list args;
  va_start(args,fmt);
  int ret = vasprintf(&buffer, fmt, args);
  va_end(args);
  if (ret < 0)
    {
    if (buffer)
      free(buffer);
    return ret;
    }
  ret = write(buffer, ret);
  free(buffer);
  return ret;
  }

ssize_t ConsoleSSH::write(const void *buf, size_t nbyte)
  {
  if (!m_ssh || (m_connection->flags & MG_F_SEND_AND_CLOSE))
    return 0;

  int ret = 0;
  if (m_drain > 0)
    {
    if (m_connection->send_mbuf.len > 0)
      {
      m_drain += nbyte;
      return 0;
      }
    if (--m_drain)
      {
      ESP_LOGE(tag, "%zu output bytes lost due to low free memory", m_drain);
      char *buffer = NULL;
      ret = asprintf(&buffer,
        "\r\n\033[33m[%zu bytes lost due to low free memory]\033[0m\r\n", m_drain);
      if (ret < 0)
        {
        if (buffer)
          free(buffer);
        return ret;
        }
      ret = wolfSSH_stream_send(m_ssh, (uint8_t*)buffer, ret);
      free(buffer);
      if (ret < 0)
        {
        ++m_drain;
        return 0;
        }
      m_drain = 0;
      ProcessChar('R'-0100);
      }
    }
  if (!nbyte)
    return 0;

  uint8_t* start = (uint8_t*)buf;
  uint8_t* eol = start;
  uint8_t* end = start + nbyte;
  while (eol < end)
    {
    if (*eol == '\n')
      {
      if (eol > start)
        {
        ret = wolfSSH_stream_send(m_ssh, start, eol - start);
        if (ret <= 0) break;
        }
      ret = wolfSSH_stream_send(m_ssh, CRLF, 2);
      if (ret <= 0) break;
      ++eol;
      start = eol;
      }
    else if (++eol == end)
      {
      ret = wolfSSH_stream_send(m_ssh, start, eol - start);
      break;
      }
  }

  if (ret <= 0)
    {
    if (ret == WS_REKEYING)
      {
      if (!m_rekey)
        ESP_LOGW(tag, "wolfSSH rekeying, some output will be lost");
      m_rekey = true;
      }
    else if (ret == WS_WANT_WRITE)
      {
      m_drain = 1;
      ret = 0;
      }
    else
      {
      ESP_LOGE(tag, "wolfSSH_stream_send returned %d: %s", ret, GetErrorString(ret));
      m_connection->flags |= MG_F_SEND_AND_CLOSE;
      }
    }
  else
    m_rekey = false;
  return ret;
  }

// Routines to be called from within WolfSSH to receive and send data from and
// to the network socket.

int RecvCallback(WOLFSSH* ssh, void* data, uint32_t size, void* ctx)
  {
  ConsoleSSH* me = (ConsoleSSH*)ctx;
  return me->RecvCallback((char*)data, size);
  }

int ConsoleSSH::RecvCallback(char* buf, uint32_t size)
  {
  mbuf *io = &m_connection->recv_mbuf;
  size_t len = io->len;
  if (size < len)
    len = size;
  else if (len == 0)
    return WS_CBIO_ERR_WANT_READ;       // No more data available
  memcpy(buf, io->buf, len);
  mbuf_remove(io, len);
  return len;
  }

int SendCallback(WOLFSSH* ssh, void* data, uint32_t size, void* ctx)
  {
  mg_connection* nc = (mg_connection*)ctx;
  nc->flags |= MG_F_SEND_IMMEDIATELY;
  size_t ret = mg_send(nc, (char*)data, size);
  if (ret == 0)
    {
    if (!((ConsoleSSH*)nc->user_data)->IsDraining())
      {
      size_t free8 = heap_caps_get_free_size(MALLOC_CAP_8BIT|MALLOC_CAP_INTERNAL);
      ESP_LOGW(tag, "send blocked on %zu-byte packet: low free memory %zu", size, free8);
      }
    size = WS_CBIO_ERR_WANT_WRITE;
    }
  return size;
  }

//-----------------------------------------------------------------------------
//    Class RSAKeyGenerator
//-----------------------------------------------------------------------------

RSAKeyGenerator* RSAKeyGenerator::s_instance = NULL;

RSAKeyGenerator::RSAKeyGenerator() : TaskBase("RSAKeyGen", 7*1024, 0)
  {
  s_instance = this;
  Instantiate();
  }

RSAKeyGenerator::~RSAKeyGenerator()
  {
  s_instance = NULL;
  }

bool RSAKeyGenerator::KillInstance()
  {
  if (!s_instance) return false;
  s_instance->Kill();
  return true;
  }

void RSAKeyGenerator::Service()
  {
  // Generate a random 2048-bit SSH host key for the SSH server in DER format.
  // The source of entropy in the WolfSSL code is the esp_random() function.
  RsaKey key;
  RNG rng;
  int ret;
  ret = wc_InitRng(&rng);
  if (ret != 0)
    {
    ESP_LOGE(tag, "Failed to init RNG to generate SSH server key, error = %d", ret);
    return;
    }
  ret = wc_InitRsaKey(&key, 0);
  if (ret != 0)
    {
    ESP_LOGE(tag, "Failed to init RsaKey to generate SSH server key, error = %d", ret);
    return;
    }
  ret = wc_MakeRsaKey(&key, 2048, 65537, &rng);
  if (ret != 0)
    {
    ESP_LOGE(tag, "Failed to generate SSH server key, error = %d", ret);
    return;
    }
  int size = wc_RsaKeyToDer(&key, m_der, sizeof(m_der));
  if (size < 0)
    {
    ESP_LOGE(tag, "Failed to convert SSH server key to DER format, error = %d", size);
    return;
    }

  // Calculate the SHA256 fingerprint of the public half of the key, which
  // is the hash of the 32-bit length of the item and the item itself for
  // the type string, the exponent, and the modulus.
  byte digest[SHA256_DIGEST_SIZE];
  Sha256 sha;
  const char* type = "ssh-rsa";
  uint32_t length[2];
  byte  exp[8];
  byte  mod[260];
  uint32_t explen = sizeof(exp);
  uint32_t modlen = sizeof(mod);
  ret = wc_RsaFlattenPublicKey(&key, exp, &explen, mod, &modlen);
  wc_InitSha256(&sha);
  length[0] = htonl(strlen(type));
  wc_Sha256Update(&sha, (byte*)length, sizeof(uint32_t));
  wc_Sha256Update(&sha, (const byte*)type, 7);
  length[0] = htonl(explen);
  wc_Sha256Update(&sha, (byte*)length, sizeof(uint32_t));
  wc_Sha256Update(&sha, exp, explen);
  int extra = 0;
  if (mod[0] & 0x80)  // DER encoding inserts 0x00 byte if first data bit is 1
    {
    ++extra;
    length[1] = 0;
    }
  length[0] = htonl(modlen+extra);
  wc_Sha256Update(&sha, (byte*)length, sizeof(uint32_t)+extra);
  wc_Sha256Update(&sha, mod, modlen);
  wc_Sha256Final(&sha, digest);
  unsigned char fp[48];
  size_t fplen = sizeof(fp);
  int rc = Base64_Encode(digest, sizeof(digest), fp, &fplen);
  if (rc != 0)
    {
    ESP_LOGE(tag, "Failed to encode SSH server key fingerprint in base64");
    return;
    }
  fp[43] = '\0';
  MyConfig.SetParamValue("ssh.info", "fingerprint", std::string((char*)fp, fplen));
  MyConfig.SetParamValueBinary("ssh.server", "key", std::string((char*)m_der, size));

  if (wc_FreeRsaKey(&key) != 0)
    ESP_LOGE(tag, "RSA key free failed");
  if (wc_FreeRng(&rng) != 0)
    ESP_LOGE(tag, "Couldn't free RNG");
  }

static void wolfssh_logger(enum wolfSSH_LogLevel level, const char* const msg)
  {
  switch (level)
    {
    case WS_LOG_AGENT:
    case WS_LOG_SCP:
    case WS_LOG_SFTP:
    case WS_LOG_USER:
    case WS_LOG_ERROR:
      ESP_LOGE(wolfssh_tag, "%s", msg);
      break;

    case WS_LOG_WARN:
      ESP_LOGW(wolfssh_tag, "%s", msg);
      break;

    case WS_LOG_INFO:
      ESP_LOGI(wolfssh_tag, "%s", msg);
      break;

    case WS_LOG_DEBUG:
      ESP_LOGD(wolfssh_tag, "%s", msg);
      break;
    }
  }

static void wolfssl_logger(int level, const char* const msg)
  {
  switch (level)
    {
    case wc_LogLevels::ERROR_LOG:
      ESP_LOGE(wolfssl_tag, "%s", msg);
      break;

    case wc_LogLevels::ENTER_LOG:
    case wc_LogLevels::LEAVE_LOG:
    case wc_LogLevels::INFO_LOG:
      ESP_LOGI(wolfssl_tag, "%s", msg);
      break;

    case wc_LogLevels::OTHER_LOG:
      ESP_LOGD(wolfssl_tag, "%s", msg);
      break;

    default:
      ESP_LOGV(wolfssl_tag, "%s", msg);
      break;

    }
  }

static void* wolfssl_malloc(size_t size)
  {
  void* ptr = ExternalRamMalloc(size);
  if (!ptr)
    ESP_LOGE(wolfssl_tag, "memory allocation failed for size %zu", size);
  return ptr;
  }

static void wolfssl_free(void* ptr)
  {
  free(ptr);
  }

static void* wolfssl_realloc(void* ptr, size_t size)
  {
  void* nptr = ExternalRamRealloc(ptr, size);
  if (!nptr)
    ESP_LOGE(wolfssl_tag, "memory reallocation failed for size %zu", size);
  return nptr;
  }
