強欲で謙虚なツボツボ

趣味の読書の書の方

SSRでreact-quillを使うときの対応(Next.js)

 

とりあえず使えればいい

何も考えずにNext.jsでreact-quillを使うとこうなる。

$ npm install --save-dev react-quill
import { useState } from 'react'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.snow.css'

export default function Quill() {
  const [value, setValue] = useState<string>()

  return (
    <ReactQuill
      theme="snow"
      value={value}
      onChange={(v) => setValue(v)}
    />
  )
}

これだと次のようなエラーが出る。

ReferenceError: document is not defined

対処法はこちらを参照(1)。

importしたreact-quillの処理が働いているのだが、SSRモードでwindow.documentが生成される前なので、エラーとなってしまう。

これはnext/dynamicを利用すると解決する。
next/dynamicを使うことで、importするのを画面のレンダリングまで待つことができる。

修正後はこうなる。

import dynamic from 'next/dynamic'
import { useState } from 'react'
import 'react-quill/dist/quill.snow.css'

const ReactQuill = dynamic(() => import('react-quill'), { ssr: false })

export default function Quill() {
  const [value, setValue] = useState<string>()

  return (
    <ReactQuill
      theme="snow"
      value={value}
      onChange={(v) => setValue(v)}
    />
  )
}

 

react-quillを使うだけであれば、ここまでで十分。

 

ReactQuillインスタンスへのアクセス

ここからは、文字数カウントなどをするためにReactQuillインスタンスへのアクセスを行うための対応。

react-quillのメソッドなどは公式を参照(2)。

import dynamic from 'next/dynamic'
import { useMemo, useRef, useState } from 'react'
import 'react-quill/dist/quill.snow.css'

const ReactQuill = dynamic(() => import('react-quill'), { ssr: false })

export default function Quill() {
  const [value, setValue] = useState<string>()
  const quillRef = useRef<any>() // 本当はuseRef<ReactQuill>にしたいけど、dynamic importしているのでanyにするしかなさそう

  // 文字数をカウントしたい
  const strCount = useMemo(() => {
    if (!quillRef.current) return

    const editor = quillRef.current.getEditor()
    const unprivilegedEditor = quillRef.current.makeUnprivilegedEditor(editor)
    return unprivilegedEditor.getLength() - 1
  }, [value])

  return (
    <>
      <ReactQuill
        ref={ref}
        theme="snow"
        value={value}
        onChange={(v) => setValue(v)}
      />
      <p>文字数:{strCount}</p>
    </>
  )
}

これだと次のようなエラーが出るので、続けて対応していく。

Property 'ref' does not exist on type 'IntrinsicAttributes & ReactQuillProps'

これについては、こちらが一番詳しかったので参照。(3)

dynamic importするときに、refを設定できるようにReactQuillPropsを継承してinterfaceを作成してあげるとできた。

import dynamic from 'next/dynamic'
import { useMemo, useRef, useState } from 'react'
import { ReactQuillProps } from 'react-quill'
import 'react-quill/dist/quill.snow.css'

const ReactQuill = dynamic(
  async () => {
    const { default: RQ } = await import('react-quill')
    interface Props extends ReactQuillProps {
      forwardedRef: any
    }
    return (props: Props) => <RQ ref={props.forwardedRef} {...props} />
  },
  { ssr: false }
)

export default function Quill() {
  const [value, setValue] = useState<string>()
  const quillRef = useRef<any>() // 本当はuseRef<ReactQuill>にしたいけど、dynamic importしているのでanyにするしかなさそう

  // 文字数をカウントしたい
  const strCount = useMemo(() => {
    if (!quillRef.current) return

    const editor = quillRef.current.getEditor()
    const unprivilegedEditor = quillRef.current.makeUnprivilegedEditor(editor)
    return unprivilegedEditor.getLength() - 1
  }, [value])

  return (
    <>
      <ReactQuill
        forwardedRef={quillRef}
        theme="snow"
        value={value}
        onChange={(v) => setValue(v)}
      />
      <p>文字数:{strCount}</p>
    </>
  )
}

 

gitの差分はこちら

https://github.com/di-kotobuki/react-quill/commit/e417f36449b57ae7867fd8a8f634a64e43ff4455

参照

  1. document is not defined · Issue #292 · zenoamaro/react-quill · GitHub
  2. react-quill - npm
  3. node.js - How to access ReactQuill Ref when using dynamic import in NextJS? - Stack Overflow

【Kali Linux】aircrack-ngを使ってみる

 

概要

以前インストールしたkali-tools-top10に含まれる「aircrack-ng」を使ってみる。
aircrack-ngでやれることはこちら

  • Monitoring: Packet capture and export of data to text files for further processing by third party tools
  • Attacking: Replay attacks, deauthentication, fake access points and others via packet injection
  • Testing: Checking WiFi cards and driver capabilities (capture and injection)
  • Cracking: WEP and WPA PSK (WPA 1 and 2)

(パケット監視、反射攻撃・認証解除・偽装アクセスポイント・パケットインジェクションによるものなどの攻撃、無線LANカードとドライバーの性能チェック、WEPおよびWPA PSKの解読)

今回はパケットの監視とWPAのクラッキングで自宅のwifiルータのSSIDを当てられるかをチュートリアルに沿ってやってみる。

 

チュートリアルの手順

  1. Start the wireless interface in monitor mode on the specific AP channel
  2. Start airodump-ng on AP channel with filter for bssid to collect authentication handshake
  3. Use aireplay-ng to deauthenticate the wireless client
  4. Run aircrack-ng to crack the pre-shared key using the authentication handshake
  1. 無線LANカードを監視モードにする
  2. airodump-ngを実行して対象アクセスポイントの認証ハンドシェイクを取得
  3. (オプションの手順なので飛ばします)
  4. WPAの事前共有鍵をパスワードリストを利用して解読

 

1. Start the wireless interface in monitor mode

1-1. 無線LANカードのインターフェース名を調べる

sudo arimon-ng

実行結果のInterface列がインターフェース名で、自身の無線LANカードは通常「wlan0」となる。
今回使用するのはwlan0。
※Driver列の末尾がphy0だと対応するaircrack-ngのバージョンに限りがあるらしい
※何も表示されない場合は、無線LANカードが監視モードに対応してないということなる。

1-2. 無線LANカードを監視モードに変更

まずは現状を確認。

iwconfig

無線でインターネットに接続しているならば、wlan0には「IEEE 802.11 ESSID "接続しているネットワーク名" ...」と表示される。
これを監視モードに変更する。(末尾の9はチャンネルの指定なのでなくても問題ないと思う。)

sudo airmon-ng start wlan0 9

再びiwconfigで確認するとwlan0は「wlan0mon」となり、「IEEE 802.11 Mode:Monitor ...」になる。
ここで、「Found 2 processes that could cause trouble. ...」と表示された場合、一応指示に従って実行したほうが良い。

sudo airmon-ng check kill

 

2. Start airodump-ng to collect authentication handshake

2-1. アクセスポイントのMACアドレスを確認

sudo airodump-ng wlan0mon

実行結果からESSIDが自身のwifiルーターの行を確認。
BSSIDカラムの値がMACアドレスで、CHカラムは先程指定した9になっている。
※他人のアクセスポイントに対してクラッキングすることは犯罪なので、BSSIDは間違えないよう確実に自身のものを使用する。

2-2. 対象のアクセスポイントを監視して、4-way認証ハンドシェイクを取得

sudo airodump-ng -c 9 --bssid 11:22:33:44:55:66 -w ./psk wlan0mon
sudo airodump-ng -c <チャンネル> --bssid <MACアドレス> -w <書き出すファイル> <インターフェース名>

出力に「WPA handshake: 11:22:33:44:55:66」と表示されたら、ハンドシェイク取得に成功しているので「ctrl + c」でairodump-ngの実行を停止する。
また、psk-01.cap, psk-01.csv, psk-01.kismet.csv, psk-01.kismet.net.xml, psk-01.log.csvが生成されていることを確認する。

airodump実行直後

airodump実行でハンドシェイク取得成功

 

3. Use aireplay-ng to deauthenticate the wireless client

オプションなので飛ばします。

 

4. Run aircrack-ng to crack the pre-shared key

パスワード辞書を用意しておく。(例えばこれ

sudo aircrack-ng -w ./password.lst -b 11:22:33:44:55:66 ./psk-01.cap
sudo aircrack-ng -w <パスワード辞書> -b <MACアドレス> <ハンドシェイク取得で書き出したファイル>

自宅のwifiルーターなのでSSIDは知っているから、わざとパスワード辞書からそのSSIDを削除したものと追加してもので結果がどうなるか試してみるといいかもしれない。

パスワード辞書の「12345678」が該当するとわかった
該当なしの場合は「KEY NOT FOUND」と表示される

 

終わったら

監視モードにしたので、ネットにはつながっていない状態となっている。
PCを再起動していつも通りネット接続できていたら元通りになっているのでおしまい。


参考

  1. https://www.aircrack-ng.org/

  2. https://www.aircrack-ng.org/doku.php?id=cracking_wpa

  3. https://github.com/aircrack-ng/aircrack-ng/blob/master/test/password.lst

  4. https://www.wikihow.jp/Kali-Linux%E3%81%A7WPA/WPA2%E3%81%AE%E7%84%A1%E7%B7%9ALAN%E3%81%AB%E4%BE%B5%E5%85%A5%E3%81%99%E3%82%8B

【VSCode】Debian Ubuntuでのインストール

概要

Kali LinuxVSCodeをインストールしました。
Linuxでのインストールが初めてだったので記録しておきます。

公式の手順はこちら。

The easiest way to install Visual Studio Code for Debian/Ubuntu based distributions is to download and install the .deb package (64-bit), either through the graphical software center if it's available, or through the command line with:

code.visualstudio.com

 

インストールパッケージをダウンロード

「.db package (64-bit)」のリンクをクリックするとダウンロードできます。

 

インストール

sudo apt install ./<file>.deb

 

注意点

パスは「./」から始めないと、パッケージが見つかりませんと言われます。

パスを「./」から始めなかった場合(失敗)

パスを「./」から始めた場合(成功)

 

参考

  1. https://code.visualstudio.com/docs/setup/linux

【Kali Linux】nmapとhydraを使ってみる

 

概要

先週Kali LinuxをPCにインストールできたから今回は実際に動かしたい。
参加応募したゆるいハッキング大会の説明にnmapとhydraとあるので、これらについて調べて体験する。

 

kali-tools-top10

ペネトレーションテストやシステム監査に必要なツール10選で、これがあれば基本的なことはだいたいできる。
今回使うnmapとhydraも含まれている。

他にもMetapackagesと呼ばれるツールのおすすめセットがいくつもあるので見てみるといいかもしれない。

 This is Kali Linux, the most advanced penetration testing and security auditing distribution.

This metapackage depends on the 10 most important application that Kali Linux provides.

https://www.kali.org/tools/kali-meta/#kali-tools-top10

インストールはこちらを実行。

sudo apt install kali-tools-top10

 

hydra

パスワードクラックを行うツールで、いわゆる「辞書攻撃」ができます。
仕組みは単純で、ユーザ名とパスワードのリストによる総当りです。

https://www.kali.org/tools/hydra/

下の例は192.168.1.123に対してssh接続でusername_list.txtのユーザ名とpassword_list.txtのパスワードで総当りを6スレッドで実行するというもの。
ユーザ名やパスワードを固定値にする際は、-l admin -P passwordのように直接入力すればいいです。

hydra -l username_list.txt -P password_list.txt -t 6 192.168.1.123 ssh

※他人のアカウントへ勝手にログインすることは不正アクセスになりえるのでしてはいけない。

 

nmap

nmapを使うと、対象が使用しているport番号とそのport番号におけるプロトコル、状態、提供されているサービス、について知ることができます。

https://nmap.org/man/ja/index.html

別のPCでReact.jsで作成したアプリをローカルで起動して、Kali LinuxをインストールしたPCからnmapを実行しました。
※他人のネットワークをスキャンすることは不正アクセスになりえるのでしてはいけない。

zsh: segmentation fault」 についてはこちらを参照

port3000でReact.jsのアプリを起動していることがnmapによってバレてしまう

 

参照

  1. https://www.kali.org/tools/kali-meta/#kali-tools-top10 
  2. https://connpass.com/event/262016/?utm_campaign=event_publish_to_follower&utm_source=notifications&utm_medium=twitter
  3. https://www.kali.org/tools/hydra/
  4. https://nmap.org/man/ja/index.html
  5. https://github.com/nmap/nmap/issues/2518

【Kali Linux】Kali LinuxをUSBを使ってPCにインストールする(ついでにSSD換装とメモリ増設)

概要

最近ITの中でもセキュリティ分野に興味を持ち出して調べたところ「ゆるいハッキング大会」というオフラインイベントがあることを知った。初心者でも参加できるみたいなので参加応募した。
Kali Linuxペネトレーション系のソフト/OSインストール済みノートPCを持参することを強く勧めているようなので、ほこりを被っていた大学生協PCにインストールする。

ついでにHDDをSSDに交換とメモリ増設をやる。元のデータは要らないのでデータを転送したりはせずに、新品のSSDにKali Linuxをインストールする。

 

 

SSDとメモリの付け替え

価格ドットコムで比較的安価なものを見繕う。

  • メモリ
    シリコンパワー ノートPC用メモリ 1.35V(低電圧)
    DDR3L 1600 PC3L-12800 8GB 204Pin Mac対応 SP008GLSTU160N02
    3090円
  • SSD
    CFD CG4VX SATA接続 2.5型SSD 480GB CSSD-S6B480CG4VX
    5399円

その他必要なものは、8GBの空っぽなUSB、ねじを開けるためのドライバー。

使ったPCは2014年に購入した富士通製で、品名「LIFEBOOK SH90/P」、型番「RMVS90PRD1」。

 

取付け作業

PCの裏側の蓋を外した状態
右上にSSD
真ん中にメモリ

元のHDDを外した部分にSSDを付ける
ピンがあるので上手く差し込む

空いているスペースにメモリを斜め上から差し込む
そしたら、上から水平になるように押し込むとピッタリはまる。

確認

PC起動時にF2キー連打でBIOS画面を表示。
メモリスロットに追加した8GBのメモリが認識されている。
ドライブ0には換装したSSDが認識されている。(後のKali Linuxインストール時にドライブ選択する場面があるので名前を覚えておく)

 

Kali Linuxインストーラーをダウンロード

公式サイトにアクセスして少し下にスクロールすると、Installer Imagesが出てくる。
Recommendedのラベルがついているものをダウンロードできるのが一番いい。
ただ、実際には時間がかかりすぎて上手くいかないので、「torrent」をクリックしてtorrentファイルをダウンロードする。

※Recommendのやつは容量は3GB弱ですが、isoファイルのダウンロードにはサーバー側での準備処理が必要だったりするらしく、かなり時間がかかるらしいです。ミラーサイトでもかなりダウンロードに時間がかかるらしいのでtorrentを利用した方が良いです。

https://www.kali.org/get-kali/#kali-installer-images

torrentファイル

ダウンロードするときにtorrentの部分をクリックすると「kali-linux-2022.3-installer-amd64.iso.torrent」というtorrentファイルになる。
簡単に言うと、isoファイルをダウンロードするための情報が詰まったファイルで、torrentを使う方が早くisoをダウンロードすることができる。

※torrentファイルは危険なことも多いようなので、出所不明なtorrentファイルには触れないように注意が必要。

torrentファイルを利用してお目当てのisoファイルをダウンロードするにはBitTorrentを使用する。

BitTorrent

「クライアントダウンロード」→BitTorrent Webの「無料ダウンロード」→無料の「今すぐダウンロード」の順に進むとインストーラーをダウンロードできる。
今回以外にも使う予定があるならClassicでもいいかもしれない。

インストーラーを起動するとWindowsセキュリティに結構怒られますが、設定から許可して動かします。
後は手順に従ってインストールを行いますが、途中MacAfeeとOperaのインストールを勧めくるので、しっかりチェックボックスは外して回避しましょう。

仕組みについてはこちらを参考

https://www.bittorrent.com/ja/

isoファイル

BitTorrentのインストールが終わると自動的にブラウザ上で立ち上がります。
Kali Linuxのtorrentファイルをドラッグアンドドロップすると、お目当てのisoファイルのダウンロードが開始されます。
5分くらいですぐ終わります。

 

Rufusをダウンロード

Rufus 3.20」をクリック。
rufus-3.20.exeがダウンロードされる。

https://rufus.ie/ja/

 

Rufusを使ってKali Linuxインストール用のUSBを作成

まず、PCにUSBメモリを挿す。(USBの中身は今からの操作で全部消されます)

※必ずUSB3.0のポートに挿しましょう。USB2.0のポートに挿すと下記の操作で「イメージの抽出に失敗しました」といったエラーでほぼ確実に失敗します。こちらを参考)

次にダウンロードしたrufus-3.20.exeを実行。
Rufusが起動したら下記の設定を行う。

  • バイス:PCに挿したUSBメモリ
  • ブートの種類:「選択」からダウンロードしたKali Linuxのisoファイルを選択
  • それ以外の部分はデフォルトのまま

「スタート」をクリック。ダイアログがいくつか表示されるがそのままOK等を選択して先へ進む。

設定後

完了
エラーとなってしまう場合に備えてログを表示しておくと良いです。
スタートボタンに左側にあるアイコンでログを表示できます。

 

USBを使ってPCにKali Linuxをインストール

Rufusで設定できたUSBをKali Linuxを入れたいPCに挿して起動します。
この時にBIOSを開くために「F2」キーを連打しておきます。(一回で良かった気がしますがタイミングが分からないので連打します。)
ピーっという音が鳴ればOKです。

BIOS画面での設定

USBメモリでブートするように設定

ヘルプに従って起動デバイスの優先順位をUSBを一番にする。
その他のデバイスは一応無効にしておく。

セキュアブートをしないように設定

「セキュアブート設定」→「セキュアブート機能」で使用しないに変更


インストール作業

BIOS画面での上記設定ができたら、保存して終了する。
すると、USBメモリでブートしてくれる。

「Graphical install」でEnterを押下するとインストール作業が始まる。

※USBでKali Linuxをインストールすると結構な確率で「インストールメディアをマウントできませんでした」という内容のエラーが発生するっぽいです。対応方法としては、USBを一回抜き挿しするとUSBを認識して大丈夫になるらしいです。こちらを参考)

言語関連の設定

日本を選択。

ネットへの接続設定

この場は無線接続で設定をした方がインストールがうまく行きます。

ドメインの設定

未設定でも大丈夫そうです。

ユーザ情報の設定

起動時のログインやsudoコマンドの実行に必要なのでしっかり覚えておく。

ディスクパーティションの設定

デフォルトの選択肢でそのまま進めば大丈夫そう。

設定を適用するために変更します。

デフォルトが「いいえ」になっているので、「はい」を選択。

ソフトウェアのインストール

下のおすすめソフトも入れたほうがいいと思いますが、インストールが失敗となった場合はチェックを外して、デスクトップ用にXfceだけをチェックするとインストールが成功します。

これでKali Linuxのインストール完了です。
Rebootするとログイン画面が表示されます。

ログイン

ユーザ情報設定の際に入力したユーザ名とパスワードを入力するとログインできる。

 

🎉

 

日本語入力

デフォルトの状態では、日本語入力を受け付けてくれないので対応します。

ターミナルを起動して、

sudo apt install -y task-japanese task-japanese-desktop

を実行。
再起動すればいつもどおりの「半角/全角」で入力できるようになります。

再起動後、画面右下に出現します。

 

おまけ(上手くいかなかったこと)

相当な回数失敗して土日を2週間分近く浪費しました。。。

USBを挿すポートで失敗(USB3.0USB2.0

Rufusを使ってインストール用USBメモリを作成するところで失敗しました。

原因は、上記でも触れているようにUSB2.0ポートを使用していたことです。

これに気づかずに、何度もtorrentファイルからisoファイルをダウンロードしなおして試してみたり、RecommendedではなくてNetInstallerのisoファイルを試してみたりと、いろいろ試行錯誤しました。

まさか、USB2.0と3.0でこんなに違うとは思わなかった。
今後はちゃんと意識した方が良さそうです。(3.0があるのに思考停止で2.0に挿していたのも考え物だ)

途中で失敗・・・

CD-ROMドライブが見つからない

原因は不明のままですが、こちらのおかげでなんとかなりました。
これが表示されたら、USBを一旦外して再度挿して、USBがピカピカ光ったのを確認してから「はい」をこの画面で選択して進めると、今度はインストールメディアをマウントできます。



ネットワーク設定の失敗

PCにはLANケーブルを挿していたので、初めは有線(eth0)を選択したのですがうまくいきませんでした。

無線(wlan0)を選択し直して進めたらうまくいきました。

eth0を選択したらエラーになった模様

 

ソフトウェアインストールの失敗とやり直し

初め、ソフトウェアインストールのところで一度失敗し(ここまでは問題ない)、よくわからずにそのまま進めてしまったことで(ここが問題)、デスクトップ用のソフトなしでKali Linuxのインストールが完了して、全てがコマンドによる操作になってしまいました。
シャットダウンすらままならなかったので、結構大変でした。

対応は、以下の手順で行いました。
rebootコマンドで再起動
再起動時にF2キー連打でBIOS画面を表示させる
起動デバイスの優先順位を変更(間違ったインストールとなったKali Linuxを起動させない)
言語の設定からやり直す
ソフトウェアのインストールは上述のようにおすすめソフトなどのチェックを外してデスクトップソフトはチェックする

※1点注意が必要なのはデバイスパーティションの設定で、すでに間違ったKali Linuxのインストールでディスクが使用されているので、この画面ではディスク全体を選択すること。

ソフトウェアインストールを失敗


参考

  1. https://connpass.com/event/262016/

  2. https://www.kali.org/get-kali/#kali-installer-images

  3. http://www.typemoon.com/tool/bittorrent.html

  4. https://www.bittorrent.com/ja/
  5. https://github.com/pbatard/rufus/issues/494
  6. https://rufus.ie/ja/
  7. https://www.motokis-brain.com/article/97

  8. https://www.web-dev-qa-db-ja.com/ja/linux/usb%E3%81%8B%E3%82%89kali-linux%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%E3%80%81cdrom%E3%83%89%E3%83%A9%E3%82%A4%E3%83%96%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84/958666238/

【Aurora MySQL】日本語対応

概要

AWS RDSでAurora MySQLを選択してデータベースを作成した。
Railsとかでmigration実行をしてテーブルも作成した後、データ挿入を行うと日本語が全て???で保存されていた。
ちゃんと日本語で保存されるように頑張る。

具体的には、AWSのコンソールでパラメータグループを作成するのと、mysqldumpで作成済のデータベースやテーブルのエンコードを再設定する。

 

元の状態

パラメータ

Aurora MySQLはデフォルトでパラメータが以下のように設定される。

show variables like '%char%';
  • character_set_client: utf8
  • character_set_connection: utf8
  • character_set_database: latin1
  • character_set_filesystem: binary
  • character_set_results: utf8
  • character_set_server: latin1
  • character_set_system: utf8

latin1なので日本語に対応できずに???となってしまう。
今回はパラメータグループの設定によって、filesystem以外をutf8mb4に変更する。(mb4は絵文字とかにも対応できる)

作成済テーブルのエンコード

show create database {データベース名}
show create table {テーブル名}

出力される情報の末尾に、DEFAULT CHARACTER SET latin1と表示される。
今回はデータベースを削除して再作成することで、DEFAULT CHARACTER SET utfmb4となるように変更する。

 

パラメータグループの作成と設定

コンソールから「Create paramter group」で作成

  • Parameter group family: aurora-mysql5.7(各自のDBに合わせる)を選択
  • Type: DB Cluster Parameter Groupを選択

作成したパラメータグループを「Edit parameters」で編集する。
編集後は「Save changes」で保存。

  • character_set_client: utf8mb4
  • character_set_connection: utf8mb4
  • character_set_database: utf8mb4
  • character_set_results: utf8mb4
  • character_set_server: utf8mb4
  • collation_connection: utf8mb4_bin
  • collation_server: utf8mb4_bin
  • skip-character-set-client-handshake: 1

作成したパラメータグループを設定するためDBクラスターを「Modify」で編集。

Additional configuration > Database options > DB cluster parameter groupで作成したパラメータグループを選択。

設定後にDBクラスターに所属するDBインスタンスを「Reboot」して設定を反映。

これで、以後作成されるデータベースとテーブルは大丈夫。

 

作成済のデータベースとテーブルのエンコードを変更

DBの踏み台にしているEC2インスタンスssh接続したのち、mysqldumpを実行。

mysqldump -u {USER_NAME} -p -h {HOST_NAME} {DB_NAME} > {OUTPUT_FILE_NAME}

出力された.sqlファイルを編集したいが、編集作業はローカルの方が楽なのでローカルへコピーする。

scp -i {鍵ファイル} {ec2-user}@xxx.xxx.xxx.xxx:{OUTPUT_FILE_NAME} ./

OUTPUT_FILEのlatin1とutf8となっている箇所を全てutf8mb4へ置換して、EC2インスタンスへコピーする。

scp -i {鍵ファイル} ./{OUTPUT_FILE_NAME} {ec2-user}@xxx.xxx.xxx.xxx:./

EC2インスタンスからDBへ接続し、データベースを削除する。

mysql -u {USER_NAME} -p -h {HOST_NAME}
drop database {DB_NAME};
quit;

EC2インスタンスから編集後の.sqlファイルを適用してデータベースを作成する。

mysql -u {USER_NAME} -p -h {HOST_NAME} {DB_NAME} < {OUTPUT_FILE_NAME}

これで完了。

 

おしまい

データベースのエンコードとかタイムゾーンみたいなパラメータの設定は、データベース作成前にきちんとやっておくといい。

後からやるとデータベースを再作成しないといけないから面倒。

 

Redis + Node.js + Socket.io でチャットアプリの動きをつける

概要

何かと必要になりがちなチャットアプリの動きを実装する。
使うのはNodejs、Redis、Websocket。
docker-composeで起動させる。
typescriptで記述するのでその辺の設定についても。

 

 

完成物

https://github.com/di-kotobuki/chat

 

イメージ図

Redisでpub/sub機能を使うと複数サーバー間で同期できる。
サーバー側でRedisに保存されているデータの各キーに対してそれぞれ接続してサブスクリプションさせておくことで、publishされた時にそれが全てのサーバーに伝えられる。

 

やること

  1. dockerの構築
  2. typescriptの準備
  3. websocketのサーバ側を実装
  4. websocketのクライアント側を実装

 

ディレクトリ構造

/
├ app
│   ├ src
│   │   ├ public
│   │   │   ├ index.html
│   │   │   └ style.css
│   │   └ index.ts
│   ├ package.json
│   ├ tsconfig.json
│   └ yarn.lock
├ .gitignore
├ docker-compose.yml
├ Dockerfile
└ README.md

 

dockerの構築

サーバ用のコンテナとRedisのコンテナを作る。

# docker-compose.yml
version: "3"

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: web
    environment:
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    volumes:
      - ./app:/usr/src/app
    ports:
      - 3000:3000
    tty: true
    depends_on:
      - redis

  redis:
    image: redis:latest
    container_name: redis
    ports:
      - 6379:6379
    volumes:
      - ./data:/data
    command: redis-server --appendonly yes
# Dockerfile
FROM node:16-alpine

RUN apk update && apk add --no-cache libc6-compat && ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2

WORKDIR /usr/src/app
COPY ./app/package.json ./app/yarn.lock ./
RUN yarn install

COPY ./app ./

EXPOSE 3000

 

typescriptの準備

サーバはtypescriptで記述したいので必要なnodeパッケージをインストールしたり、ビルドの設定を行う。

初期化

必要なnodeパッケージをインストール。

cd app
npm init -y
yarn add typescript

tsconfig

tsconfig.jsonを作成して編集する。

cd app
npx tsc --init

これでtsconfig.jsonが生成されるので、末尾にincludeとexcludeを追加してtargetとoutDirを変更。

# /app/tsconfig.json
{
  ...,
  "target": "ES6",
  "outDir": "./dist", 
  ..., 
  "include": [ "src/**/*" ],
  "exclude": [ "node_modules" ] 
}

ビルド設定

編集するたびにビルドと起動をしなくてもいいようにホットリロードの設定。

cd app
yarn add -D ts-node ts-node-dev rimraf npm-run-all

そして、package.jsonを編集。

# app/package.json
{
  "name": "chat",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "dev": "ts-node-dev --respawn src/index.ts",
    "build": "npm-run-all clean tsc",
    "start": "node ."
  },
  // 省略...
}

起動確認

さっきpackage.jsonで"dev": "ts-node-dev --respawn src/index.ts"としたので、app/src/index.tsを作成して適当に編集。

# app/src/index.ts
console.log("hello world");

yarn devでindex.tsを編集するたびにホットリロードされることを確認できる。

cd app
yarn dev

 

 

Socketのサーバー側

さっきのindex.tsにwebsocketの記述をしていく。

必要なパッケージ

ioredisとsocketをインストール。
NodejsでRedisを使うならioredisがいいらしい。
Websocketを使用するためのsocket.io。
Nodejsのフレームワークといったらのexpress。
POSTメソッドのリクエストでパラメータをパースするためのbody-parser。
日時の処理がしたいのでmoment。

cd app
yarn add ioredis socket.io socket.io-client express body-parser moment
yarn add -D @types/ioredis @types/socket.io @types/express

コード

大まかに、redisへの接続とwebsocketの実装を行う。

redisに保存できるデータ型はいくつかあるが、今回はチャットが送られた順番も大事なのでソート済セット型を使う。
ソート済セット型は「キー」「スコア」「値」から構成され、チャットを例に挙げるとキーがチャットルームID、スコアがチャット送信日時、値がチャット内容にあたる。
キーはデータベースのテーブルにあたると考えると理解しやすいかもしれない。
キーの数だけredisとの接続を行って、各接続でそれぞれのキーにおいてサブスクリプションさせる。チャットルームが閉鎖された時は該当するキーへのサブスクリプションを行っているredisへの接続を切断したらいい。

websocketはクライアント側からのメッセージ送信などのイベントを受け取り、redisへのデータ保存や他のクライアントへの転送を行う。
双方向でのやりとりができ、一方のemitに他方のonが呼応する関係。
一方でemit("hoge")を実行すると他方でon("hoge")が実行される。

# app/src/index.ts
import express from "express";
import * as http from "http";
import * as socketio from "socket.io"
import Redis from "ioredis";
import moment from "moment";
import bodyParser from "body-parser";


// 環境変数
const redis_host = process.env.REDIS_HOST;
const redis_port = parseInt(process.env.REDIS_PORT || "6379", 10);

const app: express.Express = express();
const server: http.Server = new http.Server(app);
const io: socketio.Server = new socketio.Server(server);
const redis: Redis = new Redis({ host: redis_host, port: redis_port });
const subscribers: Record<string, Redis> = {};

(async () => {
  // publicフォルダに静的ファイルを格納
  app.use(express.static("src/public"));

  // POSTのリクエストボディ
  app.use(bodyParser.urlencoded({
    extended: true
  }));
  app.use(bodyParser.json());

  // 全てのkeyを取得
  const keys = await redis.keys("*");
  for (let i = 0; i < keys.length; i++) {
    await addSubscriber(keys[i]);
  }

  // チャットルーム作成
  app.use("/create", async (req, res) => {
    const { key, id } = req.body;
    const date = moment.now();
    const message = { id: id, text: `NEW CHANNEL: ${key}`, date: date };
    await redis.zadd(key, moment.now(), JSON.stringify(message));
    await addSubscriber(key);
    res.send({ name: key, messages: [message] });
  });

  // チャットルーム閉鎖
  app.use("/delete", async (req, res) => {
    const { key } = req.body;
    subscribers[key].disconnect();
    delete subscribers[key];
    res.send("ok");
  });

  // チャットルーム取得
  app.use("/get", async (req, res) => {
    const keys = await redis.keys("*");
    const rooms = [];
    for (let i = 0; i < keys.length; i++) {
      const result = await redis.zrange(keys[i], 0, -1);
      const room = {
        name: keys[i],
        messages: result.map((msg) => JSON.parse(msg))
      }
      rooms.push(room);
    }
    res.send(rooms);
  })

  // Redis接続時の処理を登録
  io.on("connection", (socket: socketio.Socket) => {
    // クライアント側からのメッセージ送信を待ち受ける
    // クライアント側: socket.emit("send", { key: channel, id: id, text: text })
    socket.on("send", async (data) => {
      const date = moment.now();
      const message = JSON.stringify({
        id: data.id,
        text: data.text,
        date: date
      });
      await redis.zadd(data.key, date, message);
      await redis.publish(data.key, message);
      console.log(`メッセージ送信: ${message}`);
    });
  });

  server.listen(3000, () => {
    console.log("listening on *:3000")
  })
})();

// subscriberを追加
async function addSubscriber(key: string) {
  const client = new Redis({ host: redis_host, port: redis_port });
  await client.subscribe(key);
  client.on("message", (_, msg) => {
    // クライアント側: socket.on(room_name, (message) => {})
    io.emit(key, JSON.parse(msg));
    console.log(`メッセージ転送: ${msg}`);
  });
  subscribers[key] = client;
}

 

Socketのクライアント側

htmlとか結構長くなってしまうので、websocketやajaxに関する記述のみ記載

# app/src/public/index.html
<!-- importmapでcdn読み込み -->
<script type="importmap">
  {
    "imports": {
      "socket.io-client": "https://cdn.socket.io/4.4.1/socket.io.esm.min.js",
      "axios": "https://cdn.skypack.dev/axios"
    }
  }
</script>
<script type="module">
  import { io } from "socket.io-client";
  import axios from "axios";
  
  (() => {
    // io()の引数は接続するwebsocketサーバーのホストだが、今回はこの同じdockerコンテナの中なのでio()でいい。
    const socket = io();
    
    async function init() {
      document.getElementById("send").addEventListener("click", async () => {
        const el = document.getElementById("send_text");
        // サーバ側: socket.on("send", async (key: string, id: number, text: string) => {})
        await socket.emit("send", { key: channel, id: id, text: el.value });
        el.value = "";
      })
    }
    
    // メッセージ受信
    async function receiveMessage(room_name) {
      // サーバ側: io.emit(key, JSON.parse(msg));
      socket.on(room_name, (message) => {
        // メッセージ受信時の描画処理
      });
    }
    
    // チャットルーム取得
    async function getRooms() {
      axios.get("/get")
      .then((response) => {
        const rooms = response.data;
        for (let i = 0; i < rooms.length; i++) {
          // 取得したチャットルームでのメッセージを受信できるように設定
          receiveMessage(rooms[i].name);
          // チャットルーム取得時の描画処理
        }
        console.log(response);
      })
      .catch((error) => {
        console.log(error);
      });
    }
    
    // チャットルーム作成
    async function createRoom(room_name) {
      axios.post("/create", {
        key: room_name,
        id: id
      })
      .then((response) => {
        // 作成したチャットルームでのメッセージを受信できるように設定
        receiveMessage(room_name);
        // チャットルーム作成時の描画処理
      })
      .catch((error) => {
        console.log(error);
      });
    }
  })();
</script>

 

おわり

なんだかんだで二週間くらいかかってしまったけど、websocketの記述とredisを少しわかることができたのでよかった。

 

参考

Amazon ElastiCache for Redis を使ったChatアプリの開発 | Amazon Web Services ブログ

GitHub - aws-samples/elasticache-refarch-chatapp: Example architecture for building a chat application using ElastiCache for Redis.