5,000円ポッキリで自分専用のサーバ&リモートクライアントを持とう！

今回の例では、サーバは、無償で使い続けられるOCI(Oracle Cloud Infrastructure)を使います。

サーバの導入は次の記事などを参考に行います。

[OCI をFree Tier(Always Free)の範囲で触ってみる](https://qiita.com/alphajinsei/items/16d21866b53a189878df)

上記で用意した、サーバのIPアドレスと公開鍵と秘密鍵のファイルを手元に用意しておきます。

次に、リモートクライアントは、次のものを使いますので購入し、手元に用意しておきます。

[M5Stack Cardputerキット（M5StampS3付属）](https://www.switch-science.com/products/9277)

今回は、Arduino IDEでリモートクライアントのアプリを作成するので、次の記事などを参考に、Arduino IDE環境も準備します。注意点としては、2.x系ではなく、1.x系を導入します。（SPIFFSアップロードプラグインを利用するため）

[Arduino IDE Programming Environment Setup - Cardputer](https://docs.m5stack.com/ja/quick_start/Cardputer/arduino)

その後、Arduino IDEへSPFFFSアップローダープラグインを次の記事などを参考に追加します。

[ESP32-WROOM-32 SPIFFS アップローダープラグインの使い方](https://www.mgo-tec.com/blog-entry-spiffs-uploader-plugin-arduino-esp32.html)

次のsshクライアントのコードを元にアプリを作成するので、任意の場所にgit cloneしておきます。

[ESP32 Arduino SSH wrapper class](https://github.com/hpirila/ESP32-Arduino-SSH)

Arduino IDEでssh_clientというプロジェクトを新しく作成し、「ツール」→「ボード」で「M5Cardputer」を選択、「ツール」→「ライブラリを管理...」で「M5Cardputer」と「LibSSH-ESP32」を追加しておきます。

メニューで「スケッチ」→「スケッチのフォルダを表示」をクリックして表示されるフォルダ内に、上記でgit cloneしたファイルの内、main.cpp以外をコピーし、dataフォルダを新規追加し、その中に上記で用意したサーバの公開鍵、秘密鍵をコピーしておきます。

次のようなファイル配置になります。
- ssh_client
    - data
        - サーバの秘密鍵
        - サーバの公開鍵
    - ssh_client.ino
    - ssh.cpp
    - ssh.hpp
    - storage.cpp
    - storage.hpp

なお、ssh_client.inoの内容は次の通りです。こちらの内容はコピーしなかったmain.cppとM5CardputerのサンプルスケッチinputTextの内容をマージしたような内容になっています。

```c:ssh_client.ino
#include "M5Cardputer.h"
#include "M5GFX.h"
#include <WiFi.h>
#include "ssh.hpp"

String SSID = "Wi-FiのSSID";
String WIFI_PASS = "Wi-Fiのパスワード"; 

const std::string HOST = "ホストのIPアドレス";
const std::string USER = "ホストのユーザ名";
const std::string PUB_KEY = "/spiffs/公開鍵のファイル名";
const std::string PRI_KEY = "/spiffs/秘密鍵のファイル名";
const std::string PASS = "秘密鍵のパスフレーズ";

const unsigned int TASK_STACK_SIZE = 60000;
const unsigned int SEND_BUFFER_SIZE = 256;
const unsigned int RECV_BUFFER_SIZE = 1024;

M5Canvas canvas(&M5Cardputer.Display);
TaskHandle_t sshHandle = NULL;
QueueHandle_t xQueue_to_host;
QueueHandle_t xQueue_from_host;
String data = "> ";

void sshTask(void* pvParameter) {
  SSH ssh{};

  Serial.println("SSH Connecting to server...");
  ssh.connectWithKey(HOST, USER, PUB_KEY, PRI_KEY, PASS);
  delay(1000);
  if (ssh.isConnected) {
    Serial.println("SSH is connected!");
  } else {
    Serial.println("SSH connection failed.");
  }

  while (1) {
    std::string result;
    char resultStr[RECV_BUFFER_SIZE] = {};
    char buff[SEND_BUFFER_SIZE] = {};

    xQueueReceive(xQueue_to_host, &buff, portMAX_DELAY);
    if (strlen(buff) != 0) {
      if (strcmp("exit\n", buff) == 0) {
        Serial.println("Close ssh connection");
        ssh.end();
        Serial.println("Kill ssh task");
        vTaskDelete(NULL);
      }
      ssh.sendCommand(buff, result);
      strcpy(resultStr, result.c_str());
      xQueueSend(xQueue_from_host, &resultStr, 0);
    }
    delay(100);
  }
}

void setup(void) {
  Serial.begin(115200);

  WiFi.begin(SSID, WIFI_PASS);

  Serial.println("Connecting to WiFi...");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(100);
  }
  Serial.println("\nConnected to the WiFi network");
  Serial.print("Local ESP32 IP: ");
  Serial.println(WiFi.localIP());

  auto cfg = M5.config();
  M5Cardputer.begin(cfg, true);
  M5Cardputer.Display.setRotation(1);
  M5Cardputer.Display.setTextSize(0.5);
  M5Cardputer.Display.drawRect(0, 0, M5Cardputer.Display.width(),
                               M5Cardputer.Display.height() - 28, GREEN);
  M5Cardputer.Display.setTextFont(&fonts::FreeSerifBoldItalic18pt7b);

  M5Cardputer.Display.fillRect(0, M5Cardputer.Display.height() - 4,
                               M5Cardputer.Display.width(), 4, GREEN);

  canvas.setTextFont(&fonts::FreeSerifBoldItalic18pt7b);
  canvas.setTextSize(0.5);
  canvas.createSprite(M5Cardputer.Display.width() - 8,
                      M5Cardputer.Display.height() - 36);
  canvas.setTextScroll(true);
  canvas.println("SSH is connected!");
  canvas.pushSprite(4, 4);
  M5Cardputer.Display.drawString(data, 4, M5Cardputer.Display.height() - 24);


  xTaskCreatePinnedToCore(sshTask, "ctl", TASK_STACK_SIZE, NULL,
                          (tskIDLE_PRIORITY + 3), &sshHandle,
                          portNUM_PROCESSORS - 1);
  xQueue_to_host = xQueueCreate(1, 256);
  xQueue_from_host = xQueueCreate(1, 256);
}

void loop(void) {
  char buff[RECV_BUFFER_SIZE] = {};
  M5Cardputer.update();
  xQueueReceive(xQueue_from_host, &buff, 0);
  if (strlen(buff) != 0) {
    Serial.print("result from host: ");
    Serial.println(buff);
 
    canvas.println(buff);
    canvas.pushSprite(4, 4);
    M5Cardputer.Display.fillRect(0, M5Cardputer.Display.height() - 28,
                                 M5Cardputer.Display.width(), 25,
                                 BLACK);
    M5Cardputer.Display.drawString(data, 4,
                                   M5Cardputer.Display.height() - 24);          
  }

  if (M5Cardputer.Keyboard.isChange()) {
      if (M5Cardputer.Keyboard.isPressed()) {
          Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState();

          for (auto i : status.word) {
              data += i;
          }

          if (status.del) {
              data.remove(data.length() - 1);
          }

          if (status.enter) {
              data.remove(0, 2);
              canvas.println(data);
              canvas.pushSprite(4, 4);
              data += "\n";
              xQueueSend(xQueue_to_host, &data, 0);
              data = "> ";
          }

          M5Cardputer.Display.fillRect(0, M5Cardputer.Display.height() - 28,
                                       M5Cardputer.Display.width(), 25,
                                       BLACK);

          M5Cardputer.Display.drawString(data, 4,
                                         M5Cardputer.Display.height() - 24);          
      }
  }
  delay(100);
}
```

また、元にしているLibSSHのラッパーコードはそのままだと、コマンド送信時にその結果が取得できないので、その部分に少し手を入れます。

ssh.hppはsendCommand()の引数部分の定義を変更します。
```c:ssh.hpp
...
  bool sendCommand(std::string cmd, std::string& result);
...
```

ssh.cppはsendCommand()の定義を変更します。
```c:ssh.cpp
...
bool SSH::sendCommand(std::string cmd, std::string& result) {
  char buffer[256] = {};
  ssh_channel channel;
  channel = ssh_channel_new(session);

  if (channel == NULL) {
    return false;
  }

  int rc = ssh_channel_open_session(channel);
  if (rc < 0) {
    return false;
  }

  rc = ssh_channel_request_exec(channel, cmd.c_str());
  if (rc < 0) {
    return false;
  }
  
  while(!ssh_channel_is_eof(channel)) {
    rc = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
    result = buffer;
  }
  ssh_channel_close(channel);
  ssh_channel_free(channel);

  return true;
}
...
```


今回はライブラリのサイズの影響を受けアプリサイズが大きめなので、「ツール」→「Partition Scheme」では「Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)」を選択しておきます。

M5CardputerをUSBケーブルで接続し、「ツール」→「シリアルポート」で接続しているポートを選択します。(Macの場合だと、/dev/cu.usbmodemXXXX)

「ツール」→「ESP32 Sketch Data Upload」をクリックし、鍵ファイルをM5CardputerのSPIFFSに転送します。

上記全てを行った後、Arduino IDEの実行ボタンをクリックするとM5Cardputer上でリモートクライアントが実行されます。一回実行した後はUSBケーブルを抜いても電源ボタンを入れればアプリが実行されます。

今回はホストで実行したコマンドがエラーを起こした際の処理や、コマンド実行中の入力処理などは実装されておらず、一般的なsshクライアントが行える事のほんの一部しかできませんのでご注意を。

また、セキュリティの観点で言えば、Wi-Fi接続情報やホスト接続情報がハードコーディングされているため、このアプリが入れてあるM5Cardputerの管理には気をつけましょう。少なくとも重要なサーバへの接続などにはこのアプリをそのまま利用しない方が良いでしょう。

動作中の様子
![実機の写真](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21380/49f3c21d-95ff-c14c-2b1b-89c1e74a0ccb.jpeg)