実践低レベル Symbol

## ノード証明書の生成

openSSL と symbol-node-configurator を使用して証明書を作成します。

### Linux の場合

python 版の SymbolSDK インストール。
openSSL は apt とか yum で適当にインストールしておきます。
```shell
pip install symbol-sdk-python zenlog --user
```

symbol-node-configurator (微修正版)をクローン。
```shell
git clone https://github.com/ccHarvestasya/symbol-node-configurator.git
```

openSSL で秘密鍵の生成。
```shell
openssl genpkey -algorithm ed25519 -outform PEM -out ca.key.pem
```

証明書生成。
```shell
cd myproject
python3 ../symbol-node-configurator/certtool.py \
        --working cert \
        --name-ca "my cool CA" \
        --name-node "my cool node name" \
        --ca ca.key.pem
cat cert/node.crt.pem cert/ca.crt.pem | sudo tee cert/node.full.crt.pem
```


### Windows の場合

OpenSSL は[ココ](https://slproweb.com/products/Win32OpenSSL.html)からダウンロード。Win64 か Win32 は環境で選んでください。Light で良いと思います。EXE、MSI はお好みで。

python 版の SymbolSDK インストール。
```bat
pip install symbol-sdk-python zenlog --user
```

symbol-node-configurator (微修正版)をクローン。
```bat
git clone https://github.com/ccHarvestasya/symbol-node-configurator.git
```

openSSL で秘密鍵の生成。
```bat
openssl genpkey -algorithm ed25519 -outform PEM -out ca.key.pem
```

証明書生成。
```bat
cd myproject
python ../symbol-node-configurator/certtool.py ^
       --working cert --name-ca "my cool CA" ^
       --name-node "my cool node name" ^
       --ca ca.key.pem
cat cert/node.crt.pem > cert/node.full.crt.pem
cat cert/ca.crt.pem >> cert/node.full.crt.pem
```


## ノードへの接続と切断

### SSL ソケット通信するクラス

SSL ソケット通信するクラスを作成します。
一度作ったら変更する箇所は無いと思うので(きっと)、abstract にしてみたり。

```typescript:SslSocket.ts
import { X509Certificate } from 'crypto';
import fs from 'fs';
import tls, { ConnectionOptions } from 'tls';

/**
 * SSLソケット
 */
export abstract class SslSocket {
  /** コネクションオプション */
  private _connectionOptions: ConnectionOptions;

  /** X509証明書 */
  protected _x509Certificate: X509Certificate | undefined;

  /**
   * コンストラクタ
   * @param host ホスト
   * @param port ポート(デフォルト: 7900)
   * @param timeout タイムアウト(デフォルト: 3000)
   */
  constructor(
    protected readonly host: string,
    protected readonly port: number = 7900,
    private readonly timeout: number = 3000
  ) {
    const currentDir = process.cwd().replace(/\\/g, '/');
    this._connectionOptions = {
      host: this.host,
      port: this.port,
      timeout: this.timeout,
      cert: fs.readFileSync(`${currentDir}/cert/node.full.crt.pem`),
      key: fs.readFileSync(`${currentDir}/cert/node.key.pem`),
      rejectUnauthorized: false,
    };
  }

  /**
   * catapult サーバへパケットを送信する
   * @param packetType パケットタイプ
   * @param payload ペイロード
   * @param isResponse レスポンス有無(デフォルト: true)
   * @returns レスポンスデータ
   */
  protected async requestSocket(
    packetType: number,
    payload?: Uint8Array,
    isResponse: boolean = true
  ): Promise<Uint8Array | undefined> {
    return new Promise<Uint8Array | undefined>((resolve, reject) => {
      const payloadSize = payload ? payload.length : 0;
      let responseSize = 8; // ヘッダ分のサイズを前もって付与
      let responseData: Uint8Array | undefined = undefined;

      // SSLソケット接続
      const socket = tls.connect(this._connectionOptions, () => {
        // Symbolパケット生成
        const headerSize = 8;
        const packetSize = headerSize + payloadSize;
        const symbolPacketBuffer = new ArrayBuffer(packetSize);
        // Symbolヘッダー編集
        const symbolHeader = new DataView(symbolPacketBuffer);
        symbolHeader.setUint32(0, packetSize, true);
        symbolHeader.setUint32(4, packetType, true);

        if (payload) {
          // Symbolペイロード編集
          const symbolPayload = new Uint8Array(
            symbolPacketBuffer,
            headerSize,
            payloadSize
          );
          symbolPayload.set(payload);
        }

        // Symbolパケット送信
        socket.write(new Uint8Array(symbolPacketBuffer));

        if (!isResponse) {
          // ソケット切断
          socket.destroy();
        }
      });

      // SSL接続
      socket.on('secureConnect', () => {
        const peerX509 = socket.getPeerX509Certificate();
        if (!peerX509) return;
        this._x509Certificate = peerX509;
      });

      // データ受信
      socket.once('data', (data) => {
        // レスポンスデータ（ヘッダ）取得
        const nodeBufferView = Buffer.from(new Uint8Array(data).buffer);
        // レスポンスサイズチェック
        const responseDataSize = nodeBufferView.readUInt32LE(0);
        if (responseDataSize === 0) {
          socket.destroy();
          reject('empty data');
        }
        // レスポンスパケットタイプチェック
        const responsePacketType = nodeBufferView.readUInt32LE(4);
        if (responsePacketType !== packetType) {
          socket.destroy();
          reject(
            `mismatch packet type: expect: ${packetType} actual: ${responsePacketType}`
          );
        }

        // ヘッダが問題なければデータ部取得
        socket.on('data', (data) => {
          const tempResponseData = new Uint8Array(data);
          responseSize += tempResponseData.length;
          if (!responseData) {
            // 初回
            responseData = tempResponseData;
          } else {
            // 連結
            const merged = new Uint8Array(
              responseData.length + tempResponseData.length
            );
            merged.set(responseData);
            merged.set(tempResponseData, responseData.length);
            responseData = merged;
          }
          if (responseDataSize <= responseSize) {
            // 受信が終わったら終了
            socket.end();
          }
        });
      });

      // タイムアウト
      socket.on('timeout', function () {
        socket.destroy();
        reject('timeout');
      });

      // エラー
      socket.on('error', function (error) {
        socket.destroy();
        reject(error);
      });

      // 切断
      socket.on('close', function () {
        resolve(responseData);
      });
    });
  }
}
```

### SSL ソケット通信を使用するクラス

SSL ソケット通信を実際に使用するクラス。
今回は応答見るだけの簡単な物にします。

```Catpult.ts
import { SslSocket } from './SslSocket.js';

export class Catpult extends SslSocket {
  /** パケットタイプ */
  private PacketType = {
    NODE_DISCOVERY_PULL_PING: 0x111,
  };

  async conntest() {
    // ピア問合せ
    const socketData = await this.requestSocket(
      this.PacketType.NODE_DISCOVERY_PULL_PING
    );
    return socketData;
  }
}
```

### メイン

接続するノードのホスト名をコンストラクタに渡して接続します。
成功すれば Unit8Array で値が返ってきます。失敗したら undefined です。
```conntest.ts
import { Catpult } from './Catpult.js';

const catpult = new Catpult('sakia.harvestasya.com');
const resDat = await catpult.conntest();
console.log(resDat);
```

エラーメッセージ等は省いているので、上手く繋がらない場合は適宜エラーメッセージの出力を SslSocket クラスに追加してください。
自前のノードを所有の場合は、ノードのログを見てみると尚良いと思います(最初は何が何だか分からないでしょうけど、その内分かる様になります...たぶん)。