/dev/null

脳みそのL1キャッシュ

おうち Nomad & Consul クラスタの構築 - Consul 編

はじめに

前回は物理構築までやりました。今回は OS の基本セットアップと Consul のセットアップをしていきます。

d2v.hatenablog.com

OS のインストール

以下を参考に Raspberry PiUbuntu Server をインストールします。今回は Ubuntu LTS 20.04 (Pi 3/4) を選択します。

ubuntu.com

基本セットアップ

キーボード

JIS 配列のキーボードを使っているので、まずはキーボードの変更をします。

$ sudo dpkg-reconfigure keyboard-configuration

Generic 105-key (Intl) PC を選んで Japanese を選びます。それ以外の選択肢はデフォルトで OK です。

Capslock を Ctrl に変換

Capslock を ctrl に変更するために以下のファイルを変更します。

$ cat /etc/default/keyboard
...
XKBOPTIONS="ctrl:nocaps"
...

変更後再起動します。

時間周り

timezone を設定します。

$ sudo ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
$ sudo dpkg-reconfigure --frontend noninteractive tzdata

次に、systemd-timesyncd を使って、ntp サーバの設定をします。設定ファイル /etc/systemd/timesyncd.conf を以下のように変更します。

$ cat /etc/systemd/timesyncd.conf
...
[Time]
NTP=ntp.nict.jp
FallbackNTP=0.jp.pool.ntp.org
...

変更後に systemd-timesyncd を再起動します。

$ sudo systemctl restart systemd-timesyncd

Docker

Docker をインストールします。基本的には以下と同じ手順を実行します。

docs.docker.com

# リポジトリのセットアップ
$ sudo apt-get update
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
$ echo \
  "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# docker のインストール
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

上記を実行したあとユーザーを docker グループに追加します。

$ sudo usermod -a -G docker <username>

IP アドレスの固定

Raspberry Pi の IP アドレスがころころ変わると困るので、それを固定します。今回は netplan を使います。

まずは以下のファイルを作成します。固定するアドレスは 192.168.1.203/24 とします。

$ cat /etc/netplan/99-config.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: false
      dhcp6: false
      addresses: [192.168.1.203/24]
      gateway4: 192.168.1.1

以下のコマンドで設定を反映します。

$ sudo netplan apply

Consul のセットアップ

そもそも Consul とは

いよいよ Consul です。Consul はクラスタ内でどのサービスがどのノード上で動作しているかを特定する、いわゆるサービスディスカバリの機能を備えた Hashicorp のプロダクトです。

consul の実行時プロセスは agent と呼ばれ、server と client の 2 種類に分類され、server と client は共に複数ノードあってよいということになっています。server は consul に登録されているサービスの情報である catalog というものを管理しており、catalog のマスターデータは server の中でも leader という役割のノードが持っています。また、leader は Raft という分散合意アルゴリズムにしたがって選出され、catalog の更新は leader を通すことになっています。

セットアップ

Raft の性質上、server は最低でも 3 台はないと耐障害性を持たないです(3 台なら 1 台までは壊れてもなんとかなる)。前回の記事では 1 台のみ server を用意すると書きましたが、どうせならとやはり 3 台用意することにしました。

なので、構成図は以下のようになります。

f:id:d2v:20210325211135p:plain

consul をちゃんとセットアップするには以下を参考にすればいいですが、いかんせん長いのでここでは必要最小限な手順だけをピックアップして紹介します。

learn.hashicorp.com

共通

# インストール
$ export CONSUL_VERSION="1.8.0"
$ export CONSUL_URL="https://releases.hashicorp.com/consul"
$ curl --silent --remote-name \
  ${CONSUL_URL}/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_arm64.zip
$ unzip consul_${CONSUL_VERSION}_linux_arm64.zip
$ sudo chown root:root consul
$ sudo mv consul /usr/bin/

# ユーザとデータ用/設定用ディレクトリを作成
$ sudo useradd --system --home /etc/consul.d --shell /bin/false consul
$ sudo mkdir --parents /opt/consul
$ sudo chown --recursive consul:consul /opt/consul
$ sudo mkdir --parents /etc/consul.d
$ sudo chown --recursive consul:consul /etc/consul.d

# 設定ファイルの生成
## サーバの場合
$ sudo touch /etc/consul.d/server.hcl
$ sudo chown --recursive consul:consul /etc/consul.d
$ sudo chmod 640 /etc/consul.d/server.hcl

## クライアントの場合
$ sudo touch /etc/consul.d/client.hcl
$ sudo chown --recursive consul:consul /etc/consul.d
$ sudo chmod 640 /etc/consul.d/client.hcl

# サービスの定義、起動
$ sudo touch /usr/lib/systemd/system/consul.service
$ sudo systemctl enable consul
$ sudo systemctl start consul

なお、consul.service の中身はこんな感じ

[Unit]
Description="HashiCorp Consul - A service mesh solution"
Documentation=https://www.consul.io/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/consul.d/consul.hcl

[Service]
Type=notify
User=consul
Group=consul
ExecStart=/usr/bin/consul agent -config-dir=/etc/consul.d/
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGTERM
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

サーバ の設定ファイルはこんな感じで

datacenter = "home"
server = true
bootstrap_expect = 3
data_dir = "/opt/consul"

bind_addr = "{{ GetInterfaceIP \"eth0\" }}"
retry_join = ["192.168.1.200", "192.168.1.201", "192.168.1.202"]
client_addr = "0.0.0.0"

ui_config {
  enabled = true
}

クライアントはこんな感じです。

datacenter = "home"
data_dir = "/opt/consul"

bind_addr = "{{ GetInterfaceIP \"eth0\" }}"
retry_join = ["192.168.1.200", "192.168.1.201", "192.168.1.202"]

retry_join の中身はご自身の Raspberry Pi のアドレスに置き換えてください。

上記を終わらせると、http://<ui設定がenabledなIP アドレス>:8500 で以下のような consul の Web UI にアクセスすることができます。

f:id:d2v:20210325211347p:plain

これにて Consul のセットアップ終了です!

おわりに

今回は Consul のセットアップについて書きました。(やる気になれば)最後に Nomad のセットアップや Consul Connect についても書きたいと思います。

参考

qiita.com

qiita.com

wiki.archlinux.jp

hodalog.com

ブートローダーの修復

はじめに

先日、ゼロからのOS自作入門をやってたらブートローダーを消し飛ばしてしまいました。具体的に言うと /boot/efi 配下を書き換えちゃったってことです。

これによりしばらく、プライベート用の PC が動かなくなっていたのですが、無事修復できたので、後世のために記録を残しておきます。

準備

まず、以下を用意します。

  • 別の PC(今回は MacBook Pro
  • USB メモリ(OS が入るくらいのサイズ)

テキトーな Linux ディストロを USB にインストールします。

macOS だと BalenaEtcher が便利です。

www.balena.io

手順

USB ブートで Linux を立ち上げて以下を実行していきます。

調査

まずはディスクの状況を確認します。

$ sudo fdisk -l
Disk /dev/loop0: 1.2 GiB, 1277792256 bytes, 2495688 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes


Disk /dev/sda: 931.5 GiB, 1000204886016 bytes, 1953525168 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: C4D9398D-765B-4BCE-9CA3-8084194B8DB8

Device          Start        End    Sectors   Size Type
/dev/sda1        2048    1023999    1021952   499M Windows recovery environment
/dev/sda2     1024000    1226751     202752    99M EFI System
/dev/sda3     1226752    1259519      32768    16M Microsoft reserved
/dev/sda4     1259520  840779272  839519753 400.3G Microsoft basic data
/dev/sda5   840779776  842031103    1251328   611M Windows recovery environment
/dev/sda6   842033152 1945744089 1103710938 526.3G Linux filesystem
/dev/sda7  1945745408 1953523711    7778304   3.7G Linux swap


Disk /dev/sdb: 14.8 GiB, 15871246336 bytes, 30998528 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xad92e0dc

Device     Boot Start     End Sectors  Size Id Type
/dev/sdb1  *       64 2904959 2904896  1.4G  0 Empty
/dev/sdb2        3532    9035    5504  2.7M ef EFI (FAT-12/16/32)

/dev/sda2ブートローダー用のパーティションで、/dev/sda6 ルートパーティションです。

環境の再現

/dev/sda2/dev/sda6、その他諸々のデバイスをマウントして環境を再現し chroot します。

$ sudo mount /dev/sda6 /mnt/
$ sudo mount /dev/sda2 /mnt/boot/efi
$ sudo for i in /sys /proc /run /dev; do sudo mount --bind "$i" "/mnt$i"; done
$ sudo chroot /mnt

ブートローダーの再インストール

念願のブートローダーインストールです。

$ grub-install /dev/sda
$ upgrade-grub

あとは、blkid コマンドで調べた UUID と /etc/fstab にある EFI パーティションの UUID が異なっていたら、/etc/fstab を書き換えます。

$ blkid | grep EFI

あとは再起動して終わり

おわりに

これでいくらブートローダーをふっ飛ばしても怖くない

参考

docs.fedoraproject.org

askubuntu.com

macOS の launchd を使ってサービスを自動起動する

はじめに

Linux には systemd という起動処理やシステム管理を行うプログラムがあります。systemd は /etc/systemd/system 以下に xxxx.service を作り systemctl enable xxxx と実行すれば、起動時処理を追加できます。

これを macOS でもやりたいわけですが、これは launchd を使えば実現できます。

plist ファイル

launchd を使って起動時になにかのプログラムを動作させるためには plist ファイルを作る必要があります。/Library/LaunchDaemons 以下に plist ファイルを作り launchctl コマンドでロードしてやれば、plist ファイルの内容に従ってプログラムが起動します。

$ sudo launchctl load /Library/LaunchDaemons/com.consul.plist

ちなみに停止するには unload してやればいいです

$ sudo launchctl unload /Library/LaunchDaemons/com.consul.plist

参考までに、以下は僕が登録している plist ファイルの一例です。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.consul</string>
    <key>ProgramArguments</key>
    <array>
      <string>/Users/USER_NAME/Root/local/bin/consul</string>
      <string>agent</string>
      <string>-config-dir=/Users/USER_NAME/Root/etc/consul.d</string>
      <string>-node=macOS</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>NetworkState</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/tmp/consul-stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/consul-stderr.log</string>
    <key>UserName</key>
    <string>USER_NAME</string>
  </dict>
</plist>

ポイントとしては

  • RunAtLoad で起動時に実行されるようにする
  • KeepAlive で自動で再起動を試みるようにする
  • NetworkState でネットワークデバイスが有効になり、IP アドレスが割り当てられているときに限りプログラムを動作させるようにする
  • StandardOutPath, StandardErrorPath で標準出力、エラー出力をファイルに残すようにする

ってところくらいですかねー。以上!

参考

qiita.com

medium.com

LINE CTF 2021 writeup

LINE CTF 2021 にチーム Tomatosalad で参加しました。結果は 200 ポイント、 73 位でした。 僕は crypto カテゴリの問題を 2 問解きました。

babycrypto1

ソースコード

#!/usr/bin/env python
from base64 import b64decode
from base64 import b64encode
import socket
import multiprocessing

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
import hashlib
import sys

class AESCipher:
    def __init__(self, key):
        self.key = key

    def encrypt(self, data):
        iv = get_random_bytes(AES.block_size)
        self.cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + self.cipher.encrypt(pad(data,
            AES.block_size)))

    def encrypt_iv(self, data, iv):
        self.cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + self.cipher.encrypt(pad(data,
            AES.block_size)))

    def decrypt(self, data):
        raw = b64decode(data)
        self.cipher = AES.new(self.key, AES.MODE_CBC, raw[:AES.block_size])
        return unpad(self.cipher.decrypt(raw[AES.block_size:]), AES.block_size)

flag = open("flag", "rb").read().strip()

COMMAND = [b'test',b'show']

def run_server(client, aes_key, token):
    client.send(b'test Command: ' + AESCipher(aes_key).encrypt(token+COMMAND[0]) + b'\n')
    client.send(b'**Cipher oracle**\n')
    client.send(b'IV...: ')
    iv = b64decode(client.recv(1024).decode().strip())
    client.send(b'Message...: ')
    msg = b64decode(client.recv(1024).decode().strip())
    client.send(b'Ciphertext:' + AESCipher(aes_key).encrypt_iv(msg,iv) + b'\n\n')
    while(True):
        client.send(b'Enter your command: ')
        tt = client.recv(1024).strip()
        tt2 = AESCipher(aes_key).decrypt(tt)
        client.send(tt2 + b'\n')
        if tt2 == token+COMMAND[1]:
            client.send(b'The flag is: ' + flag)
            client.close()
            break

if __name__ == '__main__':
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(('0.0.0.0', 16001))
    server.listen(1)

    while True:
        client, address = server.accept()

        aes_key = get_random_bytes(AES.block_size)
        token = b64encode(get_random_bytes(AES.block_size*10))[:AES.block_size*10]

        process = multiprocessing.Process(target=run_server, args=(client, aes_key, token))
        process.daemon = True
        process.start()

攻撃者ができることは

  • iv + AES.enc(token + "test", iv) を手に入れられる
  • 一度だけ攻撃者が指定した iv', msg を使って iv' + AWS.enc(msg, iv') を手に入れられる
  • テキトーな暗号文を復号してもらえる

目標は iv + AES.enc(token + "show", iv) をサーバに渡して復号してもらうことです。iv はレスポンスの先頭に引っ付いてるので既知です。問題はどのようにして AES.enc(token + "test", iv) から AES.enc(token + "show", iv) を求めるかです。

ここでポイントとなるのは、token はちょうど 10 ブロック分でというところです。CBC モードの暗号化処理を見てみましょう。(Wikipedia より引用)

f:id:d2v:20210321112727p:plain

token がちょうど 10 ブロック分ということは、暗号化した後の token の最後の 1 ブロックを iv 、msg を "show" に設定してサーバに暗号化して貰えれば、 "show" の暗号ブロックのみを手に入れられます。

これを暗号化した token と結合すれば AES.enc(token + "show", iv) の出来上がりです。

ソルバは以下の通り

from pwn import *
from base64 import b64decode
from base64 import b64encode

AES_BLOCK_SIZE = 16 # bytes

p = remote("35.200.115.41", 16001)

def get_test_command(p) -> bytes:
    p.recvuntil("test Command: ")
    return b64decode(p.recvline(keepends=False))

def enc(p, iv: bytes, msg: bytes) -> bytes:
    p.recvuntil("IV...: ")
    p.sendline(b64encode(iv))

    p.recvuntil("Message...: ")
    p.sendline(b64encode(msg))

    p.recvuntil("Ciphertext:")
    return b64decode(p.recvline(keepends=False))

def send_command(p, msg: bytes) -> bytes:
    p.recvuntil("Enter your command: ")
    p.sendline(b64encode(msg))
    return b64decode(p.recvline(keepends=False))

iv_token_test = get_test_command(p)

iv_token = iv_token_test[:AES_BLOCK_SIZE*11]

next_iv = iv_token[-AES_BLOCK_SIZE:] # feed to oracle as iv

next_iv_show = enc(p, next_iv, b"show")
show = next_iv_show[AES_BLOCK_SIZE:]

iv_token_show = iv_token + show

result = p.recvline(keepends=False)
send_command(p, iv_token_show)

print(p.recv())
p.close()

babycrypto2

ぶっちゃけ babycrypto1 より簡単です。ソースコードは babycrypto1 とほぼ同じで、差分だけ取り出してみると

AES_KEY = get_random_bytes(AES.block_size)
TOKEN = b64encode(get_random_bytes(AES.block_size*10-1))
COMMAND = [b'test',b'show']
PREFIX = b'Command: '

def run_server(client):
    client.send(b'test Command: ' + AESCipher(AES_KEY).encrypt(PREFIX+COMMAND[0]+TOKEN) + b'\n')
    while(True):
        client.send(b'Enter your command: ')
        tt = client.recv(1024).strip()
        tt2 = AESCipher(AES_KEY).decrypt(tt)
        client.send(tt2 + b'\n')
        if tt2 == PREFIX+COMMAND[1]+TOKEN:
            client.send(b'The flag is: ' + flag)
            client.close()
            break

ポイントは

  • 最初に手に入れられる暗号が iv + AES.enc(token + "test", iv) から iv + AES.enc("Command: " + "test" + token, iv) に変わった
  • サーバに暗号化してもらえなくなった(できるのは暗号文の復号のみ)

このような状態で AES.enc("Command: " + "show" + token, iv) を求める必要があります。この問題で使えるのが iv です。

CBC モードの復号処理を見てみましょう。(Wikipedia より引用)

f:id:d2v:20210321114738p:plain

最初のブロックが平文に戻る直前に iv と xor を取っていますよね。xor の性質(同じ値で xor を計算すると 0 になる)を使って iv を改ざんすることで、復号したあとの平文を test ではなく show にすることができます。

ソルバは以下の通り

from pwn import *
from base64 import b64decode
from base64 import b64encode
from Crypto.Util.number import bytes_to_long as b2l
from Crypto.Util.number import long_to_bytes as l2b

AES_BLOCK_SIZE = 16 # bytes

p = remote("35.200.39.68", 16002)

def get_test_command(p) -> bytes:
    p.recvuntil("test Command: ")
    return b64decode(p.recvline(keepends=False))

def send_command(p, msg: bytes) -> bytes:
    p.recvuntil("Enter your command: ")
    p.sendline(b64encode(msg))
    return p.recvline(keepends=False)

iv_command_test_token = get_test_command(p)
iv = iv_command_test_token[:AES_BLOCK_SIZE]

partial_iv = b2l(iv[9:9+4]) ^ b2l(b"test") ^ b2l(b"show")
partial_iv = l2b(partial_iv)

iv = iv[:9] + partial_iv + iv[9+4:]

enc = iv + iv_command_test_token[AES_BLOCK_SIZE:]
send_command(p, enc)

print(p.recv())
p.close()

おわりに

死ぬほどやる気がでなかった

参考

ja.wikipedia.org

Eloquent Model 内の処理の委譲

はじめに

Eloquent Model の create メソッ の実装を見てやろうと Laravel Framework のコードを潜ってみたところ、なかなか面白い処理フローになっていたのでメモ

TL;DR

  • Model::create() を実行すると処理が Builder::create() に委譲される
  • 処理の委譲は __call() メソッドと ForwardCalls トレイトを使って実現される

__call()

https://laravel.com/api/8.x/Illuminate/Database/Eloquent/Model.html を見たらわかると思うが Model クラスには create() メソッドが存在しない。

また、Model クラスは他のクラスを継承していないし、use しているトレイトにもそれらしいメソッドはない。では、Model::create を実行したときに一体なにが呼び出されるのか。

実は Model クラスは __call() メソッドを実装していて、定義されていないメソッドを呼び出すと __call() メソッドに処理が移る

abstract class Model implements Arrayable, ArrayAccess, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
    use Concerns\HasAttributes,
        Concerns\HasEvents,
        Concerns\HasGlobalScopes,
        Concerns\HasRelationships,
        Concerns\HasTimestamps,
        Concerns\HidesAttributes,
        Concerns\GuardsAttributes,
        ForwardsCalls; // forwardCallTo() が実装されている

    ...(snip)...

    /**
     * Handle dynamic method calls into the model.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        if (in_array($method, ['increment', 'decrement'])) {
            return $this->$method(...$parameters);
        }

        // リレーション周りの処理はこの分岐に入るっぽい?
        if ($resolver = (static::$relationResolvers[get_class($this)][$method] ?? null)) {
            return $resolver($this);
        }

        // Model::create() を呼び出すと通る処理フローはこっち
        return $this->forwardCallTo($this->newQuery(), $method, $parameters);
    }

コードを見る限りリレーション以外の処理は最後の行で ForwardsCalls@forwardCallTo() に処理が移る (要調査)

forwardCallTo()

forwardCallTo() メソッドの実装は以下のようになっている

    /**
     * Forward a method call to the given object.
     *
     * @param  mixed  $object
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    protected function forwardCallTo($object, $method, $parameters)
    {
        try {
            return $object->{$method}(...$parameters);
        } catch (Error | BadMethodCallException $e) {
            ...(snip)...
        }
    }

実装の通り、forwardCallTo() は第 1 引数のオブジェクトに処理を委譲している

newQuery() の戻り値

結局 Model はどこに処理を委譲しているのか。これは newQuery() の戻り地を見るとわかる

処理フローはコードを見るとわかるが、いかんせん長いので結果だけをいうと最終的には newEloquentBuilder() メソッド内で Illuminate\Database\Eloquent\Builder クラスのインスタンスが生成され、これが返される

Illuminate\Database\Eloquent\Model クラス内

    /**
     * Get a new query builder for the model's table.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function newQuery()
    {
        return $this->registerGlobalScopes($this->newQueryWithoutScopes());
    }

    /**
     * Register the global scopes for this builder instance.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function registerGlobalScopes($builder)
    {
        foreach ($this->getGlobalScopes() as $identifier => $scope) {
            $builder->withGlobalScope($identifier, $scope);
        }

        return $builder;
    }

    /**
     * Get a new query builder that doesn't have any global scopes.
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newQueryWithoutScopes()
    {
        return $this->newModelQuery()
                    ->with($this->with)
                    ->withCount($this->withCount);
    }

    /**
     * Get a new query builder that doesn't have any global scopes or eager loading.
     *
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newModelQuery()
    {
        return $this->newEloquentBuilder(
            $this->newBaseQueryBuilder()
        )->setModel($this);
    }

    /**
     * Create a new Eloquent query builder for the model.
     *
     * @param  \Illuminate\Database\Query\Builder  $query
     * @return \Illuminate\Database\Eloquent\Builder|static
     */
    public function newEloquentBuilder($query)
    {
        return new Builder($query);
    }

Illuminate\Database\Eloquent\Builder クラス内

    /**
     * Set a model instance for the model being queried.
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return $this
     */
    public function setModel(Model $model)
    {
        ...(snip)...
        return $this;
    }

おわりに

Eloquent Model は自身で実装していない処理は ForwardCalls トレイトを使って、Builder クラスに処理を委譲していることがわかった

Docker Compose で Firebase のテスト環境を作る

はじめに

仕事で Firebase の Readltime Database を使う機会があり、Firebase Local Emulator Suite を使ったテスト環境を構築したのでメモ

設定ファイル

Dockerfile

Firebase CLI をインストールして、Realtime Database のエミュレータのみを起動している。 なお、Realtime Database のエミュレータは 9000 ポートにバインドされるので、ホストからアクセスしたい場合は EXPOSE でポートを公開しておく

FROM openjdk:11-jre-slim

EXPOSE 9000

RUN apt-get update && apt-get install -y wget
RUN wget -O firebase https://firebase.tools/bin/linux/latest \
    && chmod u+x ./firebase \
    && mv firebase /usr/local/bin

ENTRYPOINT ["firebase", "emulators:start", "--only", "database"]

docker-compose.yaml

これに関しては特に言うことはない

version: "3.7"

services:
  ...(snip)...
  firebase:
    build:
      context: .
      dockerfile: ./docker/firebase/Dockerfile
    volumes:
      - ./docker/firebase/firebase.json:/firebase.json

firebase.json

ポイントは Realtime Databse が bind するホストを 0.0.0.0 にするところ。 デフォルトでは localhost でリクエストを待ち受けているが、これでは firebase コンテナ外部からアクセスできないので、0.0.0.0 に変えておく必要がある

{
  "emulators": {
    "database": {
      "host": "0.0.0.0",
      "port": 9000
    }
  }
}

参考

firebase.google.com

Hugo で tailwindcss を使う

はじめに

Hugo でテーマを作ってみようと思い、そのテーマに tailwindcss を使おうと思ったので色々調べたのでここにまとめておく

準備

$ hugo new site hugo-tailwindcss
$ cd hugo-tailwindcss

必要なライブラリをインストールする

$ npm init -y
$ npm install -D tailwindcss@latest \
@tailwindcss/typography \
postcss-cli@latest \
autoprefixer@latest \
@fullhuman/postcss-purgecss

設定ファイルを作る

PostCSS の設定ファイルを作成する

$ cat <<EOT > postcss.config.js
const purgecss = require('@fullhuman/postcss-purgecss')({
    content: ['./hugo_stats.json'],
    defaultExtractor: (content) => {
        let els = JSON.parse(content).htmlElements;
        return els.tags.concat(els.classes, els.ids);
    }
});

module.exports = {
    plugins: [
        require('tailwindcss'),
        require('autoprefixer'),
        ...(process.env.HUGO_ENVIRONMENT === 'production' ? [purgecss] : [])
    ]
};
EOT

tailwindcss の設定ファイルを作成する

$ cat <<EOT > tailwindcss.config.js
module.exports = {
    plugins: [
        require('@tailwindcss/typography')
    ]
}
EOT

PurgeCSS のための設定ファイルを作成する。この設定により、ビルド時に hugo_stats.json が作成される

$ mkdir -p config/production
$ cat <<EOT > config/production/config.toml
[build]
writeStats = true
EOT

hugo_stats.json には以下のように、そのとき使われている HTML タグや CSS の class, id の情報が記載されている。PurgeCSS はこの情報を使って、使用されていない CSS のクラスを削除して、最終的に生成される CSS ファイルのサイズを削減できる

$ cat hugo_stats.json
{
  "htmlElements": {
    "tags": [
      "!doctype",
      "body",
      "div",
      "head",
      "html",
      "link",
      "meta",
      "title"
    ],
    "classes": [
      "bg-gray-200",
      "bg-white",
      "flex",
      "font-extrabold",
      "h-screen",
      "items-center",
      "justify-center",
      "p-10",
      "rounded-xl",
      "shadow-xl",
      "text-6xl",
      "text-blue-500"
    ],
    "ids": null
  }

CSSやHTMLファイルを作る

$ mkdir assets
$ cat <<EOT > assets/style.css
@tailwind base;
@tailwind components;
@tailwind utilities;
EOT

動作確認用に layouts/index.html を作成して、上記で作成した CSS ファイルを読み込む

$ cat <<EOT > layouts/index.html
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />

  {{ \$css := resources.Get "style.css" | resources.PostCSS }}
  {{ if hugo.IsProduction }}
  {{ \$css = \$css | minify | fingerprint | resources.PostProcess }}
  {{ end }}

  <link rel="stylesheet" href="{{ \$css.Permalink }}">
  <title>{{ .Site.Title }}</title>
</head>

<body class="h-screen flex justify-center items-center bg-gray-200">
    <div class="text-6xl text-blue-500 font-extrabold rounded-xl shadow-xl p-10 bg-white">Hello World</div>
</body>

</html>
EOT

動作確認

最後に動作確認をする

$ hugo server -D --gc
...
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
...

http://localhost:1313/ にアクセスして、下記の画面が表示できれば OK

今日の成果物

github.com

参考

gohugo.io

nananao-dev.hatenablog.com

gohugo.io