20200213のNode.jsに関する記事は9件です。

ElectronでcontextBridgeによる安全なIPC通信(受信編)

はじめに

Electronにおけるメインプロセスとレンダラープロセス間のやり取りに関して、セキュアなIPC通信にはcontextBridge1を使おう、という記事を前回書いたらそれなりに読んでもらえているみたいです。ありがとうございます。

その時の例として、レンダラープロセスからメインプロセスへの送信を扱いましたが、受信についてもリクエストがあったので紹介します。基本的にはStackOverFlow2からの引用です。

基本的にElectronにおけるメニュー操作はメインプロセスでハンドルすることになるので、それをレンダラープロセスへ送る際には、メインプロセスからチャンネル付きで信号を送信し、レンダラープロセスで受信時にチャンネルに従って処理を分ける、ということをするでしょう。これを目的としたcontextBridgeの利用法です。

前回からの改修点

まずは前回の記事の方法3までをお読みください。今回は前回の方法3からの改修点として次の様にしました。

  • レンダラーからメインプロセスへの送信時にはチャンネルを設定して複数の信号の送信に対応した。
  • メインプロセスから送信してレンダラープロセスで受信する部分では:
    • アプリのメニュークリックで送信(メニュー操作はメインプロセスの範疇)
    • レンダラーで受信したらHTMLに反映

方法4:レンダラーでの受信

メインプロセスのコードはmain.jsとします。preloadファイルはpreload.js、レンダラープロセスで読み込むhtmlファイルはindex.htmlとします。各ファイルの中身は次のようになります。

/* main.js, case 4(extend: send and recv) */
"use strict";
const {electron,BrowserWindow,app,ipcMain,Menu} = require('electron');
let mainWindow = null;
const CreateWindow = () => {
  mainWindow = new BrowserWindow({width: 800, height: 600,
    webPreferences: { 
      nodeIntegration: false,
      contextIsolation: true,
      preload: __dirname + '/preload.js'
    } });
  mainWindow.webContents.openDevTools(); 
  mainWindow.on('closed', function() {
    mainWindow = null;
  });

  /*menu creation*/
  const template = [ {
    label: 'File',
      submenu: [{
        label: 'Open',
        click: (menuItem, browserWindow, event) => { 
          // メニュークリック時に実行される関数//

          //ここでファイルを開いたりする(今回は暫定)//
          const openedPath = "./hogehoge.txt";
          const readData = "ファイルの中身だよ";
          //ここまででファイルは読み込んだものとする//

          //この関数でIPC送信(main to renderer)//    
          browserWindow.webContents.send(
            "openfile", //送信チャンネル名(自分で区別できるように)//
            {   //送信したいデータ一覧//
               filePath: openedPath,
               dataText: readData
            }
          ); 
        } 
      }]  
    }];

    const menu = Menu.buildFromTemplate(template)
    Menu.setApplicationMenu(menu);

    mainWindow.loadURL('file://' + __dirname + '/index.html');
}
app.on('ready', CreateWindow);

//IPC受信部//
ipcMain.on("msg_render_to_main1", (event, arg) => {
  console.log(arg); //printing "good job"
});
ipcMain.on("msg_render_to_main2", (event, arg) => {
  console.log("We are the", arg.teamName, "!");
  //printing "We are the Victories !"
});
/* preload.js, case 4 (extend)*/
const { contextBridge, ipcRenderer} = require("electron");
contextBridge.exposeInMainWorld(
  "api", {
    send: (channel, data) => {//rendererからの送信用//
        ipcRenderer.send(channel, data);            
      },
    on: (channel, func) => { //rendererでの受信用, funcはコールバック関数//
        ipcRenderer.on(channel, (event, ...args) => func(...args));
    }
  }
);
<!--index.html, case 4 (extend)-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Test</title>
  </head>
  <body>
    <button id="button1">test1</button>
    <button id="button2">test2</button>
    <div id="previewF">受信ファイル名</div>
    <div id="previewD">受信データ</div>
  </body>
  <script type = "text/javascript">
      //適当なプログラム
    const button1 = document.getElementById("button1");  

    //送信用(チャンネル名指定)//
    button1.addEventListener("click", (e)=>{
      window.api.send("msg_render_to_main1", "god job");
    });

    //送信用(チャンネル名指定)//
    button2.addEventListener("click", (e)=>{
      window.api.send("msg_render_to_main2", {teamName: "Victories"});
    });

    //受信部(チャンネル名指定)//
    window.api.on("openfile", (arg)=>{
      document.getElementById("previewF").textContent = arg.filePath;
      document.getElementById("previewD").textContent = arg.dataText;
    });
 </script>
</html>

まず、レンダラープロセスからメインプロセスへ送信する部分ですが、ボタンを二つ配置し、チャンネル名を変えて二種類の信号が送れるようになっています。メインプロセスではipsMain.on(チャンネル名, コールバック関数) でチャンネル名を指定することで、処理を分けて行えるようになります。

次に、本目的のレンダラープロセスでの受信ですが、preload.jsでのonの部分の記述がポイントです。コールバック関数名をfuncとしておいて、 ipcRenderer.on()の中ではスプレッド構文...argsを使っています。これにより、メインプロセスから送られてきた引数のうち、eventだけを取り除いてコールバック関数へ渡しています。コールバック関数はindex.html内のwindow.api.on("openfile",(arg)=>{ ... })にて記述できるので、実質的にipcRenderer.on()を直接書いていた時代と同様に利用できます。

注意点

contextBridgeはとっても良さそうなAPIですが、Electronのドキュメント3には次のように書かれています。

"The contextBridge API has been published to Electron's master branch, but has not yet been included in an Electron release."

一応、私の環境のversion7.1.9では使えていますが、いつから使えるようになったのかはちょっと不明なので、気を付けてください。

感想

コールバック関数という表現が合っているのか不安ですが、Javascriptライト勢なのでご勘弁くだされ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【MongoDB】APIログ取るのに手軽で最高だった件 (+intellijだとさらに手軽)

はじめに

気にはなっていたけど、なかなか触れる機会が無かった。。
そんな、同じクラスのあの子のような存在、それがmongoDBでした。
実際、使ってみると手軽でとても使いやすい。

こちらの記事では、簡単にインストールから導入までをまとめてみました。

MongoDBを使った開発内容

趣味の個人開発でMongoDBを利用しました。
[APIを利用したbitcoin自動売買システム]

  • bitcoin値取得にCryptWatchAPIを使用
  • bitcoin売買にBitflyerAPIを使用
  • 開発言語:Node.js
  • 開発環境:macOS Catalina
  • デプロイ環境: AWS:EC2:ubuntu18.04LTS

MongoDBはbitcoin売買時の値段と、その売買判断に使われた値のログを取りたくて使用しました。

mongoDBとは

誤解を恐れずに極端に言うと、

データをJSON形式でレコードできるデータベースです

すいません! ここでは、わかりやすさ優先しましたm(_ _)m
(玄人の方々、マサカリ投げないでください。)

他にも、
スケーラビリティ(拡張)しやすい、
スキーマレス(事前定義不要)である。 などなど、、ありますが、
詳しく知りたい方は以下のページをおすすめします。
詳しいオススメページ1
詳しいオススメページ2

APIをレコードするのに最適!

mongoの特徴

  • スキーマレスで、
  • JSON形式

これが、APIを記録するのにすごく相性がいいです。
つまり、
APIからレスポンスされたJSONをそのままインサートすることができる!
さらに、
毎回APIの構造が変わってもそのままインサート可能!!
すごい楽ですね、
スキーマレスなので難しく考えず、ひとまず突っ込んで置くことができます。

これが、RDBMSだとどうなるか?

  • スキーマを細かく定義した上で、例外処理を施し構造が変わるものは省いてインサートする。
  • string,textとしてまるごとインサート

どちらも大変です。。
受け取る可能性のあるJSON内容を想定して、スキーマを細かく定義した上で、
処理は、レスポンスされたJSONをカラムに収まるように全部分解したり、例外処理を書いたり。。
2つ目は、まる投げでインサートすればいいのですが、columnがTEXT形式でも文字数制限あります。
mysqlだと最大長が65,535(216 − 1)文字になります。データを利用するときはまた、JSON形式に戻して。。
うぅ、吐きそうです(;´Д`)
しかも、ノード同士の構造を捨てちゃってます。

「今は何に使うかわかんないけどー、
あとで、面白いことに使いたいから、とりあえずサクッとデータ残しておこう♪」

こんなノリには、ぜひMongoDBです。

私のbitcoin売買システムも、
将来、趣味で分析したり、機械学習で回せたらオモロイだろうなーってノリだけです。
使えそうな値と、APIをまるごと、とりあえずインサートおく。
APIの構造が多少変わっても気にしない。

まずはその手軽さを触ってみることをオススメします。
それでは、簡単に導入方法をまとめます。

インストール

macOS

brewを使って、簡単にインストールできます。

brew install mongodb

#自動起動に設定
brew services start mongodb

ubuntu18.04LTS

ubuntuはやや面倒です。
aptの管理ライブラリが最新のMongDBとなっていないため、
Mongoの公式から、パッケージを展開してインストールする必要があります。

#パッケージ管理システムに公開鍵を登録
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4

#ダウンロード用のリストファイルを作成
echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list

#パッケージ管理システムのデータベースをリロード
sudo apt-get update

#最新の安定版をインストール
sudo apt-get install -y mongodb-org

#MongoDBを自動起動にする
sudo systemctl enable mongod

#MongoDBを起動
sudo service mongod start

raspbrerryPi

ラズパイにMongDBの最新バージョンはインストールできません。

正しくは、
ラズパイの公式OS:RasbianOSには、MongDB version2.4.14以上はインストールできません。

上記のMongoDBバージョンが、64bit対応のみとなっていますが、
RasbianOSが、32bitとなっているためです。

実は、ラズパイ自体は64bitのため
OSに引きづられて最新のMongoDBが利用できなくなっています。
ですので、OSを入れ替えたら使用できるかもしれません。
詳しくはこちら

mongo2.4以下だと、npmのmongoライブラリも対応していないため、
ラズパイでmongoDBを扱うのは特別な理由が無い限りあまりおすすめしないです。

実装

データベースの設定

ターミナルからmongoにデータベースを作成して、利用できるようにします。

# MongoDBに入る
mongo

# データベース切り替え、作成
use db_name

#データベースの確認
show dbs

#使用しているデータベースを確認
db

#コレクションの作成
db.createCollection("collect_name")

#コレクションを確認
show collections

#コレクションにドキュメントをインサート
db.collect_name.insertOne({ name: "hoge", age: 88})

#コレクション内のドキュメントを確認
db.collect_name.find()

# MongoDBから抜ける
exit

ちょっと説明

  • use db_name: データベースは無ければ、自動で作成されます。
  • collection: mongoではtableでは無く、collectionと言います。
  • document:mongoでは1つ1つのデータ(record)のことをdocumentと言います。

コード実装

Node.jsで実装しました。
以下はベースの実装例です。

const MongoClient = require('mongodb').MongoClient;
const options = {
  useUnifiedTopology : true,
  useNewUrlParser : true
};
const url = 'mongodb://localhost:27017';
const dbName = 'db_name';

//即時関数と、asyncを定義
(async function () {
  let client;

  try {
    client = await MongoClient.connect(
        url,
        options,
    );
    //DB取得
    const db = client.db(dbName);

    //DBを操作
    await insertDoc(db);
    await findDoc(db);
  } catch (err) {
    //接続失敗
    console.log(err);
  }

  //接続を切る
  client.close();
})();

//Insert
function insertDoc(db) {
  const collection = db.collection('collect_name');
  collection.insertMany(
      [{ name: 'hoge', age: 88 }, { name: 'fuga', age: 14 }],
      (err, result) => {
        console.log('Success inserted');
      },
  );
}

//Find
function findDoc(db) {
  const collection = db.collection('collect_name');
  collection.find({}).toArray((err, docs) => {
        //検索内容をコンソール出力
        console.log(docs);
      });
}

データ確認

bitcoinの売買データをmongoで取得してみました。

標準のデータ確認

#terminalからの標準のデータ確認
{ 
"_id" : ObjectId("5df9c2ba73160d276ad2e3ad"), 
"flag" : "buy", 
"label" : "買い注文", 
"created_at" : ISODate("2019-12-18T06:10:02.072Z"), 
"strTime" : "2019/12/18 15:10:02", 
"price" : 737491, 
"shortMA" : 737293.825779211, 
"longMA" : 736227.9333333333, 
"countHigh" : 4, 
"records" : [ 735413, 735522, 735314, 735516, 735691, 735913, 736276, 736316, 736366, 736788, 736586, 736472, 735565, 735245, 735327, 735259, 735677, 735745, 736058, 736238, 736448, 736713, 736388, 736426, 736954, 737198, 736975, 737479, 737479, 737491 ] 
}

一部説明します

  • _id: mongoから自動で振られるid
  • created_at: ISODATE形式、datetime形式でinsertすることができます。
  • shortMA: 過去5回分のpriceの移動平均金額を算出
  • longMA: 過去30回分のpriceの移動平均金額を算出
  • records: 過去のpriceリスト。リスト形式でそのままinsertしています。

データの形式がlistでも、さらにツリーが入れ子状態になっても、insertすることができます。

Intellij(jetBrains)を利用

CLIからだと、RDBMSよりも一覧性なくて見ずらいですね。データの閲覧はIDEや、ソフトをオススメします。
私は、IntelliJを使っています。
一度だけ接続設定をすれば、それ以後、ダブルクリックだけで、DBに接続してこのようにデータ閲覧できます。
ターミナルでコマンドを打つよりも、データ確認が手軽で早くて、見やすいです。

本来は上記のようなJSON形式のnodeツリーが、テーブル表示で一覧できます。
スクリーンショット 2020-02-12 13.54.32.png
クリックひとつでソートもできますし、絞り込みも簡単です。
ツリー形式での閲覧もできます。
スクリーンショット 2020-02-12 14.06.57.png

transposed table 日本語で何形式と言うのでしょう?こんな閲覧方法もできます。
スクリーンショット 2020-02-12 14.06.12.png

強いて不満を言えば、
table viewのとき、列の順番がバラバラとなるのが気になります。
(そもそもMongoDBにはDDLで読み取れるようなculumnの順番という概念がないのでしょうが無いです。)

おわりに

MongDBは、お手軽で使い勝手がよいDBです。
APIデータを、今の時点ではどんな風に活用するか厳密に決めていないけど、
ひとまず導入しておいて後で考えるには、MongoDBは良い選択肢だと思います♪

最後までお読みいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VPSで複数のNodeJSアプリをHTTPS化してホストする

VPSで複数のサービスのAPIサーバーを運用するための手順です。

概要

  • Digital Ocean の Ubuntu イメージ(5USD/month)
  • サブドメインに各アプリを紐づける
  • Let’s Encrypt で SSL 化

サーバーのセットアップ

Digital Ocean Droplets のセットアップ

以下の設定でDropletsを作ります。
※ 他社のVPSサービスを利用する場合は、ドメインの@ www app1 app2をサーバーに向けてSSH接続できる状態までやってこの項を飛ばしてください。

  • Image: Ubuntu
  • Plan: Standard / 5USD
  • Region: シンガポール
  • Authentication: SSH keys

ドメインの管理画面から使うドメインのwww @ app1 app2 を上記で作ったサーバーに向けます。

# SSH 接続 できることを確認
$ ssh root@yourdomain.com

メモリのスワップ領域を作る

月額5USDなどの安いVPSを利用しているとメモリ不足になることがあります。事前に十分な仮想メモリを設定しておきます。

# スワップ領域が未指定であることを確認
$ free
# スワップディレクトリの作成
$ sudo mkdir /var/swap/
# スワップファイルを作成
$ sudo dd if=/dev/zero of=/var/swap/swap0 bs=1M count=1024
$ sudo chmod 600 /var/swap/swap0
# 上記のファイルをスワップ領域として割り当てる
$ sudo mkswap /var/swap/swap0
$ sudo swapon /var/swap/swap0
# 再起動で再設定できるようにする
# fstab に `/var/swap/swap0 swap swap defaults 0 0` を追記します。
$ sudo vi /etc/fstab
# 動作確認
$ free

ログインユーザーを作る

Root ユーザーでログインし続けるのはセキュリティ的によくないので新しいユーザーを追加します。

# ユーザーの追加
$ adduser taro
# 作ったユーザーにsudo権限を与える
$ usermod -aG sudo taro
# ssh 接続できるようにする
$ rsync --archive --chown=taro:taro ~/.ssh /home/taro

ファイアウォールの設定

ファイアウォールを有効化してSSH接続のみを許可するようにします。

# OpenSSHを許可
$ ufw allow OpenSSH
# 有効化
$ ufw enable
# 動作確認
$ ufw status

VSCODEの設定

VSCODEからリモートサーバーのファイルを編集できるようにします。

  • extentions remote sshをインストールします。
  • Remote-SSH: connect to host…
  • Add new Host
  • ssh root@yourdomain.com

NGINX で HTTPS を使う

Nginx のインストール

新しく作ったユーザーで Nginx をインストールします。

# インストール
$ sudo apt update
$ sudo apt install nginx

# firewallにnginxを許可します
$ sudo ufw allow 'Nginx Full'

# 動作確認
$ systemctl status nginx

yourdomain.com にブラウザからアクセスしてnginxのデフォルト画面が表示されることを確認します。

Nginx の設定ファイル

/etc/nginxにNginxの設定ファイル一式があります。その中から/etc/nginx/conf.d/home.confを作って編集していきます。

/etc/nginx/conf.d/home.conf
server {
    server_name [yourdomain].com www.[yourdomain].com;

    location / {
        root     /var/www;
        index index.html;
    }
}
server {
    server_name app1.[yourdomain].com;

    location / {
        root     /var/app1;
        index index.html;
    }
}

設定を読み込みんでnginxを再起動します。

# テスト
$ service nginx configtest
# 再起動
$ systemctl reload nginx

var/www/index.html, var/app1/index.html を適当に作ってブラウザでURLにアクセスすると各index.htmlが表示されるはずです。

Let’s EncryptでHTTPSの設定

# certbot のインストール
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt install python-certbot-nginx

# 証明書をリクエスト
$ sudo certbot --nginx -d www.[yourdomain].com -d [yourdomain].com -d app1.[yourdomain].com
# 証明書の再発行設定
$ sudo certbot renew --dry-run

https でアクセスできるようになります。

NodeJSアプリケーションをNginxでリバースproxyする

nodeJSのインストール

# インストール
$ curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ sudo apt-get install build-essential
# 動作確認
$ node -v
$ npm -v

デモアプリを動かしてみる

$ mkdir ~/app1
$ cd ~/app1
$ npm init
$ npm install --save express

VS code で上記で作ったディレクトリにアクセスしてデモアプリを作ります。[フォルダを作る] > [/home/taro/app1/]

index.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => res.send('Hello World From NodeJS!'));
app.listen(port, () => console.log(`Example app listening on port ${port}!`));

アプリを起動して動作を確認します。

# サーバーに接続して実行
$ node index.js
# 別のターミナルを起動して動作を確認
$ curl http://localhost:3000

注意: この段階では、yourdomain.com:3000 にアクセスして動作確認できません。

PM2をインストールしてアプリを永続化します

$ sudo npm install pm2@latest -g
$ pm2 start index.js
# 動作確認
$ curl http://localhost:3000

Nginx Reverse Proxy server の設定

Nginx Reverse Proxy server を設定してhttp://localhost:3000を外にだします。/etc/nginx/conf.d/home.confを編集します。

/etc/nginx/conf.d/home.conf
...
server {
    server_name app1.[yourdomain].com;
    location / {
        proxy_pass http://localhost:3000;
    }
    listen 443 ssl; # managed by Certbot
...

$ sudo systemctl restart nginxnginxを再起動します。
app1.[yourdomain].com にブラウザでアクセスするとのHello World From NodeJS!が表示されるはずです。

別のドメインを追加してHTTPS化する

home.confに新しいドメインの設定を追加します。

/etc/nginx/conf.d/home.conf
server {
    server_name app2.[yourdomain].com;

    location / {
        root  /var/app2;
        index index.html;
    }
}

Let’s EncryptでHTTPS証明書を発行します。

# nginxの再起動
$ systemctl reload nginx
# 証明書のリクエスト
$ sudo certbot --nginx -d app2.[yourdomain].com
# 証明書の再発行設定
$ sudo certbot renew --dry-run

https で app2.yourdomain.com にアクセスできます。

その他

  • Digital Ocean で Droplets の破壊作成を繰り返すとき、DestroyではなくRebuildを選択するとIPが引き継がれるので便利です。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[2020] VPSで複数のNodeJSアプリをHTTPS化してホストする

VPSで複数のサービスのAPIサーバーを運用するための手順です。

概要

  • Digital Ocean の Ubuntu イメージ(5USD/month)
  • サブドメインに各アプリを紐づける
  • Let’s Encrypt で SSL 化

サーバーのセットアップ

Digital Ocean Droplets のセットアップ

以下の設定でDropletsを作ります。
※ 他社のVPSサービスを利用する場合は、ドメインの@ www app1 app2をサーバーに向けてSSH接続できる状態までやってこの項を飛ばしてください。

  • Image: Ubuntu
  • Plan: Standard / 5USD
  • Region: シンガポール
  • Authentication: SSH keys

ドメインの管理画面から使うドメインのwww @ app1 app2 を上記で作ったサーバーに向けます。

# SSH 接続 できることを確認
$ ssh root@yourdomain.com

メモリのスワップ領域を作る

月額5USDなどの安いVPSを利用しているとメモリ不足になることがあります。事前に十分な仮想メモリを設定しておきます。

# スワップ領域が未指定であることを確認
$ free
# スワップディレクトリの作成
$ sudo mkdir /var/swap/
# スワップファイルを作成
$ sudo dd if=/dev/zero of=/var/swap/swap0 bs=1M count=1024
$ sudo chmod 600 /var/swap/swap0
# 上記のファイルをスワップ領域として割り当てる
$ sudo mkswap /var/swap/swap0
$ sudo swapon /var/swap/swap0
# 再起動で再設定できるようにする
# fstab に `/var/swap/swap0 swap swap defaults 0 0` を追記します。
$ sudo vi /etc/fstab
# 動作確認
$ free

ログインユーザーを作る

Root ユーザーでログインし続けるのはセキュリティ的によくないので新しいユーザーを追加します。

# ユーザーの追加
$ adduser taro
# 作ったユーザーにsudo権限を与える
$ usermod -aG sudo taro
# ssh 接続できるようにする
$ rsync --archive --chown=taro:taro ~/.ssh /home/taro

ファイアウォールの設定

ファイアウォールを有効化してSSH接続のみを許可するようにします。

# OpenSSHを許可
$ ufw allow OpenSSH
# 有効化
$ ufw enable
# 動作確認
$ ufw status

VSCODEの設定

VSCODEからリモートサーバーのファイルを編集できるようにします。

  • extentions remote sshをインストールします。
  • Remote-SSH: connect to host…
  • Add new Host
  • ssh root@yourdomain.com

NGINX で HTTPS を使う

Nginx のインストール

新しく作ったユーザーで Nginx をインストールします。

# インストール
$ sudo apt update
$ sudo apt install nginx

# firewallにnginxを許可します
$ sudo ufw allow 'Nginx Full'

# 動作確認
$ systemctl status nginx

yourdomain.com にブラウザからアクセスしてnginxのデフォルト画面が表示されることを確認します。

Nginx の設定ファイル

/etc/nginxにNginxの設定ファイル一式があります。その中から/etc/nginx/conf.d/home.confを作って編集していきます。

/etc/nginx/conf.d/home.conf
server {
    server_name [yourdomain].com www.[yourdomain].com;

    location / {
        root     /var/www;
        index index.html;
    }
}
server {
    server_name app1.[yourdomain].com;

    location / {
        root     /var/app1;
        index index.html;
    }
}

設定を読み込みんでnginxを再起動します。

# テスト
$ service nginx configtest
# 再起動
$ systemctl reload nginx

var/www/index.html, var/app1/index.html を適当に作ってブラウザでURLにアクセスすると各index.htmlが表示されるはずです。

Let’s EncryptでHTTPSの設定

# certbot のインストール
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt install python-certbot-nginx

# 証明書をリクエスト
$ sudo certbot --nginx -d www.[yourdomain].com -d [yourdomain].com -d app1.[yourdomain].com
# 証明書の再発行設定
$ sudo certbot renew --dry-run

https でアクセスできるようになります。

NodeJSアプリケーションをNginxでリバースproxyする

nodeJSのインストール

# インストール
$ curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ sudo apt-get install build-essential
# 動作確認
$ node -v
$ npm -v

デモアプリを動かしてみる

$ mkdir ~/app1
$ cd ~/app1
$ npm init
$ npm install --save express

VS code で上記で作ったディレクトリにアクセスしてデモアプリを作ります。[フォルダを作る] > [/home/taro/app1/]

index.js
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => res.send('Hello World From NodeJS!'));
app.listen(port, () => console.log(`Example app listening on port ${port}!`));

アプリを起動して動作を確認します。

# サーバーに接続して実行
$ node index.js
# 別のターミナルを起動して動作を確認
$ curl http://localhost:3000

注意: この段階では、yourdomain.com:3000 にアクセスして動作確認できません。

PM2をインストールしてアプリを永続化します

$ sudo npm install pm2@latest -g
$ pm2 start index.js
# 動作確認
$ curl http://localhost:3000

Nginx Reverse Proxy server の設定

Nginx Reverse Proxy server を設定してhttp://localhost:3000を外にだします。/etc/nginx/conf.d/home.confを編集します。

/etc/nginx/conf.d/home.conf
...
server {
    server_name app1.[yourdomain].com;
    location / {
        proxy_pass http://localhost:3000;
    }
    listen 443 ssl; # managed by Certbot
...

$ sudo systemctl restart nginxnginxを再起動します。
app1.[yourdomain].com にブラウザでアクセスするとのHello World From NodeJS!が表示されるはずです。

別のドメインを追加してHTTPS化する

home.confに新しいドメインの設定を追加します。

/etc/nginx/conf.d/home.conf
server {
    server_name app2.[yourdomain].com;

    location / {
        root  /var/app2;
        index index.html;
    }
}

Let’s EncryptでHTTPS証明書を発行します。

# nginxの再起動
$ systemctl reload nginx
# 証明書のリクエスト
$ sudo certbot --nginx -d app2.[yourdomain].com
# 証明書の再発行設定
$ sudo certbot renew --dry-run

https で app2.yourdomain.com にアクセスできます。

その他

  • Digital Ocean で Droplets の破壊作成を繰り返すとき、DestroyではなくRebuildを選択するとIPが引き継がれるので便利です。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js と Babel で ES6

なんか、いつも、Node.js で、ちょっとコード書いて試したり、勉強がてらコード書いたりするときに、 ES6なコード書きたい時どうすんだっけ? と悩んでしまうので、ここにメモしておきます。

実際に確認した時のそれぞれのバージョンは以下になります。

$ node --version
v12.14.1
$ npm --version
6.13.4
$ npx --version
6.13.4

あと、このメモ作成時にインストールされる Babel 関連パッケージのバージョンは、以下の通り。

$ grep babel package.json
    "@babel/cli": "^7.8.4",
    "@babel/core": "^7.8.4",
    "@babel/node": "^7.8.4",
    "@babel/preset-env": "^7.8.4"

作業用のディレクトリを作る

mkdir work

npm init を実行

npm init -y

babel 関連をインストール

npm install --save-dev @babel/core @babel/preset-env @babel/cli @babel/node

.babelrc を追加

{
  "presets": ["@babel/preset-env"]
}

スクリプトを実行するには

index.js を実行するには以下のようにする。

npx babel-node index.js

最後に自分に向かって一言

これ、シェルスクリプトにしとけば良いよね? > 自分

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nowでプロキシを建ててCORSエラーを爆速解決

前置き

nuxt generateで作った静的サイトをGitHub Pagesにホスティングし,自分のはてなブックマークのRSSをAxiosでGETして表示させようとしていました.

何も考えずにブラウザで開くとこんなエラーが出てうまくいきません.

Access to XMLHttpRequest at 'http://api.example.com' from origin 'http://localhost.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

いわゆる,オリジン間リソース共有(CORS)における同一オリジンポリシー違反です.このエラーはブラウザにおいて,現在アクセスしてるサイトと異なるオリジンにあるリソースに対してリクエストを行うときに起きます.

今回の場合,CORS解決に必要なヘッダー情報がはてな側からのレスポンスに含まれていないことが原因で,ブラウザによって通信をブロックされてしまいました.CORS解決に必要なヘッダーというのはAccess-Control-Allow-Origin: *のようなフィールドです.

参考: https://qiita.com/umechiki/items/82dd43cd1465de5f5afe

ちなみに,Nuxtでよく紹介されている@nuxtjs/proxyを使った回避方法はフロントエンド側だけで解決できますが,こいつはnuxt generateでは機能しません.まさに,こちらの方と同じ状況になりました.

以上のことから,代理でRSSを取得するプロキシを建てて,適切なヘッダを付与してレスポンスを返してくれるようにすれば,CORSを解決することができます.
この記事では,Zeit社のNowを使ってCORSエラーを簡単に解決できたのでメモします.

Now

NowはZeit社のPaaSで,シンプルを極めたようなデプロイ方法が特徴です.
Zeitのアカウントを作り,デプロイ対象のディレクトリで

$ npm i -g now
$ now login
$ now deploy

と実行していけば,ほぼ3ステップでデプロイできてしまいます.

Serverless Functionsを使うにはapiというディレクトリを作り,その中にtarget.jsを置いておくとデプロイ時に自動的にビルドされ,https://<project-name>.<username>.now.sh/api/targetでアクセスできるようになります.

参考: https://zeit.co/docs/v2/serverless-functions/introduction

代わりにRSSを取って来てもらう

api下に置くコードは,AxiosでGETしたはてなブックマークのレスポンスBodyにCORS解決に必要なヘッダを付与してリクエスト元に返すようにすればOKです.

api/target.js
const axios = require('axios')

module.exports = async (req, res) => {
  await axios.get('https://b.hatena.ne.jp/<username>/bookmark.rss?of=1')
  .then((hatenaRes) => {
    res.setHeader('content-type', hatenaRes.headers['content-type'])
    res.setHeader('Access-Control-Allow-Origin', 'https://<username>.github.io')
    res.status(200).send(hatenaRes.data)
  }).catch((e) => {
    console.log(e)
    res.status(500).send('Internal Server Error.')
  })
}

全て許す場合はres.setHeader('Access-Control-Allow-Origin', '*')を設定してください.

これははてなのRSSを取るための小さな構成例ですが,req.headersres.setHeader()を使うことでより細かな制御を行うことができます.Access-Control-Allow-Originの設定だけでは解決できない場合もあるので,いろいろ実験してみてください.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NestJS CLIで初心者でも簡単にNode.js REST APIが作れる!

Node.jsなんてほとんど使ったことがないのに、頑張ってNestJSでCLIを使ってREST APIを作りました。
あまりドキュメントなかったことに加えて私のスキル不足で半日ほどかかってしまいましたが慣れれば30分もかからないです。

環境

Ubuntu18.04.01 LTSで動かしました。
NEST CLIのnest infoでバージョン情報を見ます。

$ nest info

 _   _             _      ___  _____  _____  _     _____
| \ | |           | |    |_  |/  ___|/  __ \| |   |_   _|
|  \| |  ___  ___ | |_     | |\ `--. | /  \/| |     | |
| . ` | / _ \/ __|| __|    | | `--. \| |    | |     | |
| |\  ||  __/\__ \| |_ /\__/ //\__/ /| \__/\| |_____| |_
\_| \_/ \___||___/ \__|\____/ \____/  \____/\_____/\___/


[System Information]
OS Version     : Linux 4.15
NodeJS Version : v12.14.1
NPM Version    : 6.13.4 

[Nest CLI]
Nest CLI Version : 6.14.2 

[Nest Platform Information]
platform-express version : 6.10.14
common version           : 6.10.14
core version             : 6.10.14

手順

NestJS 入門を参考にしました。作ったプログラムはGitHubに置いてあります。

1. プロジェクト作成

nest new <プロジェクト名>でプロジェクトを作ります。

$ nest new nest-test
⚡  We will scaffold your app in a few seconds..

CREATE /nest-test/.eslintrc.js (599 bytes)
CREATE /nest-test/.prettierrc (51 bytes)
CREATE /nest-test/README.md (3370 bytes)
CREATE /nest-test/nest-cli.json (64 bytes)
CREATE /nest-test/package.json (1877 bytes)
CREATE /nest-test/tsconfig.build.json (97 bytes)
CREATE /nest-test/tsconfig.json (336 bytes)
CREATE /nest-test/src/app.controller.spec.ts (617 bytes)
CREATE /nest-test/src/app.controller.ts (274 bytes)
CREATE /nest-test/src/app.module.ts (249 bytes)
CREATE /nest-test/src/app.service.ts (142 bytes)
CREATE /nest-test/src/main.ts (208 bytes)
CREATE /nest-test/test/app.e2e-spec.ts (630 bytes)
CREATE /nest-test/test/jest-e2e.json (183 bytes)

? Which package manager would you ❤️  to use? npm
✔ Installation in progress... ☕

?  Successfully created project nest-test
?  Get started with the following commands:

$ cd nest-test
$ npm run start

2. プロジェクト作成確認

プロジェクト作成時の指示に従い、nodeを起動します。

$ cd nest-test
$ npm run start

起動したのでcurlで確かめてみます。"Hello World!"を取得できました。もちろんブラウザでアクセスしても同じ結果です。

$ curl http://localhost:3000
Hello World! 

3. Module作成

CLIからnest generate module <モジュール名>でModuleを生成。今回はModuleなどをすべてtestで統一しています。

nest generate module test
CREATE /src/test/test.module.ts (81 bytes)
UPDATE /src/app.module.ts (308 bytes)

4. Interface作成

CLIからnest generate interface <インターフェース名>でInterfaceを生成。インタフェースはフォルダ付きで"test/test"にしています。他と違うので気持ち悪いのですが、こんなものなのでしょうか。

nest generate interface test/test
CREATE /src/test/test.interface.ts (25 bytes)

生成されたtest.interface.tsを修正します。Interfaceに型を定義します。

test.interface.ts
export interface Test {
    id: string;
    name: string;
}

5. Service作成

CLIからnest generate service <サービス名>でServiceを生成。

$ nest generate service test
CREATE /src/test/test.service.spec.ts (446 bytes)
CREATE /src/test/test.service.ts (88 bytes)
UPDATE /src/test/test.module.ts (155 bytes)

手でtest.service.tsを修正します。複数のTestを出力する関数listTestとidを指定して単一のTestを返す関数getTestを定義します。

test.service.ts
import { Injectable } from '@nestjs/common';
import { Test } from './test.interface';

@Injectable()
export class TestService {
  private readonly test: Test[] = [{id: 'a', name: 'Alex'}, {id: 'b', name: 'Bob'}, {id: 'c', name: 'Cathy'}];

  listTest(): Test[] {
    return this.test;
  }

  getTest(id: string): Test {
    return this.test.find(value => value.id === id);
  }
}

6. Controller作成

CLIからnest generate controller <コントローラー名>でControllerを生成。

$ nest generate controller test
CREATE /src/test/test.controller.spec.ts (479 bytes)
CREATE /src/test/test.controller.ts (97 bytes)
UPDATE /src/test/test.module.ts (240 bytes)

test.controller.tsを修正。先ほどサービス内で定義した関数を呼び出します。
@Controller('test')とすることでパスtestにアクセスしたときに動作するようにしています。@Get()としたことでGETメソッドでアクセスしたときに動作します。@Get(':id')は、パスtestの配下に指定したidを受け取ります。

test.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { TestService } from './test.service';
import { Test } from './test.interface';

@Controller('test')
export class TestController {
    constructor(private readonly testService: TestService) {}

    @Get()
    listUsers(): Test[] {
      return this.testService.listTest();
    }

    @Get(':id')
    getTest(@Param('id') id: string): Test {
      return this.testService.getTest(id);
    }      
}

7. 起動と確認

テストのためにnodeを起動。

nest start

curlで結果を確認します。無事、リスポンスを取得できました。

$ curl http://localhost:3000
Hello World

$ curl http://localhost:3000/test
[{"id":"a","name":"Alex"},{"id":"b","name":"Bob"},{"id":"c","name":"Cathy"}]

$ curl http://localhost:3000/test/a
{"id":"a","name":"Alex"}

8. ビルド

最後にnest builddistフォルダ配下にコードをビルドします。

nest build
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoogleのCloud Text-to-Speechを使ってDiscordの読み上げbotをサクっと作った

Discordのメッセージ読み上げbot

Discordのボイスチャットで、特定のチャンネル内のメッセージを自動で読み上げてくれるbotを作りました。

Discordの読み上げbotとしては喋太郎という有名なものが既に存在しますが、自分で作ってみたくなったので作りました。

GitHub

https://github.com/kotofurumiya/helmholtz

作った背景

作ってみたくなったからです。以上。

というのも味気ないのでちゃんとした話をします。

Discordのボイスチャットは便利ですが、どうしても「今の時間帯は声出せない」とか「飯食ってる」とかでマイクをミュートにせざるをえない人が発生します。あるいは単純に「聞き専」な人も。

そういった人たちとコミュニケーションをとろうとするとなかなか難しく、「聞き専」用のテキストチャンネルを作り、そこにメッセージを書き込んでもらうことでなんとか双方向のやりとりを成り立たせていました。しかし会話に熱中にしていると聞き専チャンネルへの書き込みにどうしても気づけなかったり、ゲームの対戦中なんかだとメッセージが来たのがわかっていてもチラ見するのも難しかったりします。

そんな中で「聞き専チャンネルに書き込まれたメッセージを読み上げるbotがあればいいんじゃね?」と思いついたのがきっかけです。テキスト読み上げがあればメッセージが来たのに気がつくし、チャット欄に目線を動かさなくても何を言っているのか把握しやすくなります。読み上げbotさえあれば全部解決するんだろうなーと思い作り始めました。

名前の「ヘルムホルツ」はグラブルの武器から取りました。なんか音出しそうな感じの武器の名前ないかな〜と探してたらヘルムホルツが目についたので採用しました。

なお喋太郎のことを知ったのは作り終わってからです。まあいい勉強になったので良しとします。

機能

基本的な機能としてはテキスト読み上げしかありません。あとは自動入室・自動退室周りの機能ぐらいです。

  • マイクミュートの人が発言するとそのボイスチャットチャンネルに自動参加
    • 指定された特定のテキストチャンネルのみ対応
  • マイクミュートの人の発言を自動で読み上げてくれる
  • ボイスチャットに誰もいなくなったら自動退室

思想としては「ユーザは一切の操作不要。勝手に動く」を中心としています。

技術

使った技術自体はシンプルです。

私にしては珍しくTypeScriptを採用せず生のJavaScriptを115行ゴリゴリ書いて動かしてます。中身もだいぶ雑です。

Discordの制御に関してはdiscord.jsという便利なライブラリがあったので使わせていただきました。テキスト読み上げ音声はCloud Text-to-Speechを使用しています。デプロイはDockerでコンテナ化してContainer Registoryにpushし、GCEにデプロイしています。ずっと動き続けるタイプのbotなのでGCEを選びました。

discord.js

discord.jsはDiscordを簡単に操作するためのライブラリです。チャンネルやメッセージの操作、ボイスチャットへの参加や発話などができます。DiscordのAPIについて調べなくてもbotを動かすことができたので、大変助かりました。

https://discord.js.org/

困った点としては、discord.jsの音声再生周りが新しめのNode.jsと相性がよろしくないらしく、音声が途中で途切れるという問題が発生しました。結局、音声再生問題の出ないNode.js v8までバージョンを下げることになったのですが、サポート期間とか考えるとだいぶ不安です。

Cloud Text-to-Speech

GoogleのCloud Text-to-Speechはテキストから音声合成を提供してくれるサービスです。Speech-to-Text(音声から文字起こし)の逆ですね。

https://cloud.google.com/text-to-speech?hl=ja

1ヶ月あたり400万文字まで無料で使えるので、ちょっとした使い方なら無料枠をオーバーすることはありません。ガンガン使っていけます。ただし他のText-to-Speechサービスと同じく日本語はあまり流暢ではなく、少し機械的な感じになってしまいます。そこは我慢しましょう。

制作期間

ライブラリが大体揃っていたので1日ぐらいでできました。先述の音声周りのバグには苦しめられましたが、あとはそんなに詰まるポイント無かったです。

実際に導入してみて

このbotを導入してから2ヶ月ほど経ちましたが、メンバーからの評判は良いです。今まで断絶が起こっていた聞き専に近い人たちとの交流も活発になりました。喋れない/喋りたくないという人でもボイスチャットに参加できるようになって、だいぶ賑やかになりました。

喋太郎を導入しても同様の効果は得られたとは思いますが、やはり自分で作るのは楽しかったです。皆さんも「車輪の再発明」とか気にせず作りたいものをガンガン作っていけばいいと思います。その方がきっと楽しいです。

さいごに

Discordって機能複雑だしボイチャ周りとかややこしそ〜というイメージがありますが、今は各種ライブラリが整備されているので簡単に作ることができます。発想次第で色々できると思うので、bot作りに手を出してみてはどうでしょう。

こうやって日常生活の片手間に小さな問題を解決していけると、とても楽しいです。このbotは規模としては非常に小さなものですが、ちょっとだけでも誰かの役に立てているという実感はとても大きく響いてきます。

実は手を出していないだけで、小さな労力で自分が活躍できる領域というのはきっといろんな場所に散らばっていると思うので、そういう細かな課題をどんどん見つけてどんどん解決していきましょう。

それでは、プログラミング楽しんでいきましょう〜。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-cli-plugin-express でポート番号を指定してサーバーを起動する

vue-cli 3.x プラグインの vue-cli-plugin-express にて、ポート番号を指定してサーバーを起動する際に詰まったので備忘録として。

この記事は vue のプロジェクトに vue add express でプラグインを追加した状態から進めていきます。

解決方法

package.json"scripts" 内にある "express" または "express:run" の値の末尾に --port [任意のポート番号] を付け加えることで、ポート番号を指定してのサーバー起動に成功しました。

package.json
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "express": "vue-cli-service express:watch --port 3001",
    "express:run": "vue-cli-service express:run --port 3001"
  }
> npm run express

> test@0.1.0 express (プロジェクトのパス)
> vue-cli-service express:watch --port 3001

 DONE  Wed Feb 12 2020 23:22:41 GMT+0900 (GMT+09:00) 

  ♻️  Server running at:
    - Local:   http://localhost:3001/
    - Network: http://192.168.0.100:3001/


  ⚙  You're in development mode. to start the application, run npm run serve.

  �? Fallback to this server enabled: you can use relative routes in your cod

  �? No api routes found (yet?).

解決前に試したこと

プラグイン導入時に生成される ./srv/index.js 内に app.listen(port, () => {}) の形で書いてもポート番号が反映されず、プラグインの README.md にポート番号の指定について書かれていなかったのでしばらく悩みました。

node_modules の中を直に弄るのもなぁと思いながらファイルを漁ってみたところ、それっぽいことが書いてあるファイルが見つかりました。
vue-cli-plugin-express/src/config.js (GitHub)

この時点でなんとなく指定方法がわかったのですが、何かの出力では…?と思い、 npm run express を実行したときに呼び出される vue-cli-service express:watch に対して、 --help をつけて呼び出してみたところ、それらが出力されました。

> npx vue-cli-service express:watch --help

  Usage: vue-cli-service express:watch [options]

  Options:

    --delay   delays run by a small duration (default: false)
    --host    specify host (default: 0.0.0.0)
    --port    specify port (default: 3000)
    --https   use https (default: false)

  For more info, see https://github.com/mathieutu/vue-cli-plugin-express

今後はちゃんと --help を使っていこうと思いました…。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む