メインコンテンツまでスキップ

· 約8分
ogumaru

概要

自分用に複数のアプリケーションを稼働させているサーバにおいて、以下に対応するため構成を変更することにした。

  1. 今後は SiYuan のようなログイン機能を持たないアプリケーションも自分用に安全に公開したい。
  2. PlantUML は VSCode の拡張機能からも利用できるようにベーシック認証を設定したい。
  3. 現状は Shiori, Trilium Notes などそれぞれで別アカウントでのログインが必要なので、シングルサインオン(SSO)に対応したい。
  4. SSL/TLS 証明書はcertbot-renew.timerを利用して各サブドメインごとに更新していたが、*.example.com のようなワイルドカード証明書を取得したい。
  5. HTTP3 を使ってみたい。

方針

以下の理由から、ルーティングは既存の nginx から Traefik へ変更することにした。

  • 既存環境が Docker Compose 上に構築されていること。
  • dnsChallenge の指定で DNS-01 チャレンジ によるワイルドカード証明書取得ができること。
  • fowardauth によりユーザ認証を挟めること。
  • 試験的ではあるが HTTP3 を有効にできること。 (nginx では自分でビルドが必要)

ユーザ認証には Keycloak も検討したが、当該インスタンスのディスク容量が少ないことから、よりサイズの小さい Authelia を採用した。

メモリ使用量については比較ができなかったが、Go の方が少ないだろうという思い込みもある。

項目AutheliaKeycloak
Docker イメージサイズ小 (約 55MB)大(約 450MB)
実装言語GoJava

構成の概要図は以下の通り。

概要図

各アプリケーションの認証方式は以下のようになる。

アプリケーション旧構成新構成
Shiori独自認証独自認証
Trilium Notes独自認証パスワード認証 (SSO)
PlantUML認証なしベーシック認証 (SSO)

また、Authelia 自身のログインには Web Authn による二段階認証を設定する。

Traefik の設定

traefik.yml は以下の通りとした。

検証の際は caServer: https://acme-staging-v02.api.letsencrypt.org/directory を有効にする。

traefik.yml
global:
checknewversion: true
sendanonymoususage: false

experimental:
http3: true

entryPoints:
web:
address: :80
# httpsにリダイレクト
http:
redirections:
entrypoint:
to: webSecure
scheme: https
webSecure:
address: :443
http:
tls: true
http3: {}

api:
insecure: false
dashboard: true

providers:
file:
directory: /etc/traefik/
docker:
exposedByDefault: false

certificatesResolvers:
myresolver:
acme:
email: hoge@example.com
storage: /letsencrypt/acme.json
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
dnsChallenge:
provider: gandiv5
resolvers:
- "ns-a.example.com:53"
- "ns-b.example.com:53"
- "ns-c.example.com:53"
keyType: EC384

compose.yamlのコンテナ設定は以下。

Gandi でドメインを取得しているため、ワイルドカード証明書取得のために GANDIV5_API_KEY を環境変数として渡す。

ミドルウェアとして指定されている auth@docker は後述の Authelia のコンテナを指している。

compose.yaml
services:
traefik:
image: "traefik:v2.9"
container_name: traefik
ports:
- "80:80"
- "443:443"
environment:
GANDIV5_API_KEY: "${GANDIV5_API_KEY}"
labels:
traefik.enable: true
traefik.http.routers.api.service: api@internal
traefik.http.routers.api.rule: Host(`traefik.example.com`) && PathPrefix(`/api`)
traefik.http.routers.api.entrypoints: webSecure
traefik.http.routers.api.tls.certresolver: myresolver
traefik.http.routers.api.middlewares: auth@docker

traefik.http.routers.dashboard.service: dashboard@internal
traefik.http.routers.dashboard.rule: Host(`traefik.example.com`) && PathPrefix(`/dashboard`)
traefik.http.routers.dashboard.entrypoints: webSecure
traefik.http.routers.dashboard.tls.certresolver: myresolver
traefik.http.routers.dashboard.middlewares: strip-auth
traefik.http.middlewares.strip-auth.chain.middlewares: auth@docker,dashboard-stripprefix
traefik.http.middlewares.dashboard-stripprefix.stripprefix.prefixes: /dashboard

traefik.http.routers.wildcard-certs.tls.certresolver: myresolver
traefik.http.routers.wildcard-certs.tls.domains[0].main: example.com
traefik.http.routers.wildcard-certs.tls.domains[0].sans: "*.example.com"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# rootless-docker
# - //run/user/1000/docker.sock:/var/run/docker.sock:ro
- ${PWD}/traefik/:/etc/traefik/:ro
- ${PWD}/letsencrypt/:/letsencrypt/
restart: unless-stopped

Authelia の設定

鍵の値など秘密情報があるが、それらのパラメータは環境変数ではなく secrets に指定して渡す。

コンテナ側の secrets で利用するものを指定した上で、/run/secrets/ 内のファイル名にアクセスすると、ファイルの内容が展開されて参照することができる。

A secret value can be loaded by Authelia when the configuration key ends with one of the following words: key, secret, password, or token.

Secrets - Configuration - Authelia - (www.authelia.com)

compose.yamlsecrets 部分の記述は以下。

secrets:
authelia_jwt_secret_file:
file: "${PWD}/secrets/authelia/jwt_secret.txt"
authelia_session_secret_file:
file: "${PWD}/secrets/authelia/session.secret.txt"
authelia_storage_encryption_key_file:
file: "${PWD}/secrets/authelia/storate.encryption_key.txt"

compose.yaml のコンテナの設定は以下。

compose.yaml
auth:
container_name: auth
image: authelia/authelia:4.37
restart: unless-stopped
command: ["--config", "/config/configuration.yml"]
volumes:
- ${PWD}/authelia-data/config/:/config/
environment:
TZ: "Asia/Tokyo"
AUTHELIA_JWT_SECRET_FILE: "/run/secrets/authelia_jwt_secret_file"
AUTHELIA_SESSION_SECRET_FILE: "/run/secrets/authelia_session_secret_file"
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: "/run/secrets/authelia_storage_encryption_key_file"
labels:
traefik.enable: true
traefik.http.routers.auth.rule: Host(`auth.example.com`)
traefik.http.routers.auth.entryPoints: webSecure
traefik.http.routers.auth.tls.certresolver: myresolver

traefik.http.middlewares.auth.forwardAuth.address: http://auth:9091/api/verify?rd=https%3A%2F%2Fauth.example.com%2F
traefik.http.middlewares.auth.forwardAuth.trustForwardHeader: true
traefik.http.middlewares.auth.forwardAuth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email
traefik.http.middlewares.auth-basic.forwardAuth.address: http://auth:9091/api/verify?auth=basic
traefik.http.middlewares.auth-basic.forwardAuth.trustForwardHeader: true
traefik.http.middlewares.auth-basic.forwardAuth.authResponseHeaders: Remote-User,Remote-Groups,Remote-Name,Remote-Email
deploy:
resources:
limits:
cpus: "1.0"
memory: 250M
secrets:
- authelia_jwt_secret_file
- authelia_session_secret_file
- authelia_storage_encryption_key_file

traefik.http.middlewares.auth-basic はベーシック認証のミドルウェア、 traefik.http.middlewares.auth はパスワード認証のミドルウェアとなる。

argon2 による認証の場合、ログイン時に負荷が高まるためリソース制限したほうが安定した。

configuration.yml は以下。

configuration.yml
theme: light
# AUTHELIA_JWT_SECRET_FILE
# jwt_secret: ****
default_2fa_method: webauthn
server:
host: 0.0.0.0
port: 9091
path: ""
enable_pprof: false
enable_expvars: false
disable_healthcheck: false
headers:
csp_template: ""
# csp_template: "default-src 'self'; frame-src 'none'; object-src 'none'; style-src 'self' 'unsafe-inline' 'nonce-********'; frame-ancestors 'none'; base-uri 'self'"

authentication_backend:
file:
path: /config/users.yml
watch: false
search:
email: false
case_insensitive: false
password:
algorithm: argon2
argon2:
variant: argon2id
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16

webauthn:
disable: false
display_name: "Hoge Auth"
attestation_conveyance_preference: indirect
user_verification: preferred
timeout: 60s

session:
name: authelia_session
domain: example.com
same_site: strict
# AUTHELIA_SESSION_SECRET_FILE
# secret: ****
expiration: 7d
inactivity: 12h
remember_me_duration: 1M

access_control:
default_policy: deny
rules:
- domain: "auth.example.com"
policy: two_factor
- domain: ["traefik.example.com", "trilium.example.com"]
policy: one_factor
subject:
- "group:admin"
- domain: "plantuml.example.com"
policy: one_factor
subject:
- "group:admin"
- "group:guest"

storage:
local:
path: /config/db.sqlite3
# AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
# encryption_key: ****

notifier:
filesystem:
filename: /config/notification.txt

argon2 のパラメータについては RFC 9106 の以下を参考に設定した。

If much less memory is available, a uniformly safe option is Argon2id with t=3 iterations, p=4 lanes, m=2^(16) (64 MiB of RAM), 128-bit salt, and 256-bit tag size. This is the SECOND RECOMMENDED option.

VSCode での PlantUML 利用時、ベーシック認証情報を平文で書く必要があるため、PlantUML 用のゲストユーザ (group:guest) を作成し plantuml.example.com のみ許可するようにした。

各アプリケーションの設定

それぞれ利用する認証方式にあったミドルウェアを指定した。

compose.yaml
shiori:
image: ghcr.io/go-shiori/shiori:v1.5.4-6-g888e59d
container_name: shiori
volumes:
- "${PWD}/shiori:/shiori"
labels:
traefik.enable: true
traefik.http.routers.shiori.rule: Host(`shiori.example.com`)
traefik.http.routers.shiori.entrypoints: webSecure
traefik.http.routers.shiori.tls.certresolver: myresolver
environment:
- PUID=1000
- PGID=1000
restart: unless-stopped

plantuml:
image: plantuml/plantuml-server:jetty-v1.2023.1
container_name: plantuml
command: --module=http-forwarded
labels:
traefik.enable: true
traefik.http.routers.plantuml.rule: Host(`plantuml.example.com`)
traefik.http.routers.plantuml.entrypoints: webSecure
traefik.http.routers.plantuml.tls.certresolver: myresolver
traefik.http.routers.plantuml.middlewares: auth-basic@docker
restart: unless-stopped

trilium:
image: zadam/trilium:0.59.3
container_name: trilium
volumes:
- "${PWD}/trilium-data:/home/node/trilium-data"
environment:
- USER_UID=1000
- USER_GID=1000
labels:
traefik.enable: true
traefik.http.routers.trilium.rule: Host(`trilium.example.com`)
traefik.http.routers.trilium.entrypoints: webSecure
traefik.http.routers.trilium.tls.certresolver: myresolver
traefik.http.routers.trilium.middlewares: auth@docker
restart: unless-stopped

独自ログイン機能の無効化

Shiori はログイン機能を無効にできなかったため、Authelia による認証をバイパスする。

Trilium Notes は Disable authentication の通り config.ini で以下設定を行うと可能だった。

[General]
noAuthentication=true

不要設定を解除

従来利用していた SSL/TLS 証明書の自動更新を無効化する。

sudo systemctl disable certbot-renew.timer
# > Removed symlink /etc/systemd/system/timers.target.wants/certbot-renew.timer.

使用感

それ自身が重いため、移行前から PlantUML の利用は安定しなかったが、それ以外については t4g.micro のインスタンス( RAM 512 MB, スワップ 1GB) でも使用感に問題はなかった。

· 約5分
ogumaru

概要

Git のコミット署名に利用している YubiKey の PIN を 3 回間違えてしまい、ロックされてしまった。

ロックされた状態でgit commitすると下記のエラーとなった。

error: gpg failed to sign the data
error: unable to sign the tag

解除コードを利用してロックの解除を行う。

環境

項目種類 / バージョン
ハードウェアキーYubikey 5 NFC
ykmanYubiKey Manager (ykman) version: 4.0.7
yubico-piv-toolyubico-piv-tool 2.2.0
gpggpg (GnuPG) 2.2.27

また、git configで下記の設定をしており、コミットに署名するようになっている。

commit.gpgsign=true
user.signingkey=********

主鍵は別途保管し、副鍵から生成した署名鍵を YubiKey に入れて管理している。

結論

YubiKey の持つ機能の内、GPG スマートカードとしての PIN がロックされているため、下記の通り対話的に GPG コマンドを実行して解除する。

gpg --card-edit

プロンプトが gpg/cardになったらadminコマンドで管理コマンドを有効化する。

gpg/card> admin
Admin commands are allowed

passwdコマンドで2を選択し、PIN のブロックを解除する。

gpg/card> passwd
gpg: OpenPGP card no. … detected

1 - change PIN
2 - unblock PIN
3 - change Admin PIN
4 - set the Reset Code
Q - quit

Your selection? 2
PIN unblocked and new PIN set.

ブロックが解除されたらqを入力し終了する。

前提知識: YubiKey の入力コードについて

YubiKey では以下の入力コードが出てくる

当初これを正しく認識しておらず、混乱した。

名称主な用途初期値
PIN (User PIN)ユーザ認証 (これを間違えてロックされた)123456
Admin PIN不明 (今回これを利用)12345678
PUKPIN のブロック解除 (今回利用しない)12345678
Management keyエンティティの認証 (今回利用しない)0102030405060708 の 3DES

参考:

だめだった方法

ykman

設定時にインストールしていたため、最初このツールでの解除を図った。

ドキュメント内からUnblock the PIN (using PUK).と書かれているものがあったためこれを実行した。

WARNING: PC/SC not available. Smart card protocols will not function.

のようなエラーが出た場合はここを参考にsudo systemctl start pcscd.serviceすることで無事実行できるようになる。

ykman piv access unblock-pin
# > Enter PUK:
# > Enter a new PIN:
# > PIN unblocked

しかしgit commitを実行しても PIN のロックは解除できていなかった。

yubico-piv-tool

上記とは別のツールがあったためこれを利用して解除を試みた。

yubico-piv-tool -a unblock-pin
# > Enter puk:
# > Enter new pin:
# > Verifying - Enter new pin:
# > Successfully unblocked the pin code.

若干出力は異なるものの、概ねykmanと同様の出力となり、同じくロックの解除はできていなかった。

再発防止のための暫定的な対応

大文字/小文字、打ち間違いなどを考慮するとリトライ回数が 3 回では不足が考えられたので、下記の通り設定を変更した。

yubico-piv-tool -a verify -P "${YUBIKEY_PIN}" -a pin-retries --pin-retries=5 --puk-retries=5

参考: PIN/PUK のリトライ回数の変更 - YubiKey PIV Manual - (tech.yubion.com)

· 約4分
ogumaru

概要

Amazon Linux 2 の EPEL 版 nginx を Docker の nginx に移行する。

併せて、Docker で稼働する他のアプリケーションコンテナも Docker Compose を利用して同一ネットワークにて管理する。

環境

項目バージョン / 種類
EC2 インスタンスタイプt4g.nano
OSAmazon Linux release 2 (Karoo)
nginx (EPEL)nginx version: nginx/1.12.2
nginx (docker)nginx:1.23.3
DockerDocker version 20.10.13, build a224086
Docker ComposeDocker Compose version v2.14.2

詳細

Amazon EC2 上に リバースプロキシとして振る舞う nginx の Web サーバがあり、これを通じてDocker 版の Shioriを利用できるようにしている。

概要図

上図左の通り、移行前の構成ではパッケージ版の nginx が外部からリクエストを受ける。

この nginx からproxy_passにより、内部で稼働するアプリケーションコンテナ(shiori)へ転送している。

移行後の構成としては上図右のように、ホスト側の443番ポートを Docker Compose のネットワーク内で稼働する nginx へ接続し、ここで外部からのリクエストを受ける。

この nginx からproxy_passにより、同じネットワーク内にあるアプリケーションコンテナへ転送する。

外部とは Let's Encrypt の証明書を利用した HTTPS 接続を行う。

Certbot はひとまず現行と同じく EPEL リポジトリのものを利用する。

各種設定ファイル

ファイルの配置は以下の通りとする

├── docker-compose.yaml
├── nginx
│ └── conf.d
│ └── shiori.example.com
└── shiori
├── archive
├── shiori.db
└── thumb

docker-compose.yaml

version: "3"

services:
nginx:
image: nginx:1.23.3
container_name: nginx
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
- /var/www:/var/www:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
ports:
- 80:80
- 443:443
restart: always

shiori:
image: ghcr.io/go-shiori/shiori:v1.5.3-35-g27c2fc7
container_name: shiori
volumes:
- "${HOME}/shiori:/shiori"
environment:
- PUID=1000
- PGID=1000
restart: always

/etc/letsencryptは Let's Encrypt の証明書関連のファイルを参照するためにマウントする。

/var/wwwcertbotによる証明書更新の際、.well-knownnginxから外部に公開するためにマウントする。

この更新のために、80番ポートもバインドする。

/nginx/conf.dには後述する設定ファイルを配置する。

shioriの部分については、もともと下記のようなスクリプトで稼働していたものから移行した。

docker run --rm -d /
--restart=always \
--name shiori \
-v "$(pwd)/shiori:/shiori" \
-u "$(id -u):$(id -g)" \
-p 8080:8080 \
shiori:v1.5.3-35-g27c2fc7

nginxが同一ネットワークになるため、shiori8080番ポートは外部に公開(publish)はしない。

nginx の設定ファイル

nginx の設定ファイルは以下の通りhttps://shiori.example.comでアクセスできる設定とする。

server {
listen 80;
server_name shiori.example.com;

location ^~ /.well-known {
alias /var/www/.well-known;
}

location / {
return 301 https://$host$request_uri;
}
}

server {
server_name shiori.example.com;
listen 443 http2 ssl;
<!-- 各種設定 -->

ssl_certificate /etc/letsencrypt/live/shiori.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/shiori.example.com/privkey.pem;

location / {
proxy_pass http://shiori:8080/;
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

<!-- 各種設定 -->
}
}

Certbot

Certbot による証明書の自動更新をcertbot-renew.timerにて行っていたため、Hook の設定も下記の通り変更する。

sudoedit /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh
  #!/bin/bash

- systemctl reload nginx
+ docker exec nginx nginx -s reload

動作確認

# 既存のWebサーバの停止
sudo systemctl stop nginx

# 既存のアプリケーションコンテナの停止
docker stop shiori

docker compose up -d

# 証明書の更新、Hookの確認
sudo certbot --dry-run renew

· 約2分
ogumaru

概要

devcontainer 内にて、WebAssembly の npm パッケージをwasm-pack packするとエラーが発生する。

結論

Node.js を devcontainer 側にインストールする。

環境

項目バージョン
cargo1.64.0 (387270bc7 2022-09-16)
wasm-pack0.10.3
dockerversion 20.10.14, build a224086

詳細

mcr.microsoft.com/vscode/devcontainers/rust:0-bullseyeを元にした devcontainer 内に、下記にてwasm-packをインストールしていた。

cargo install wasm-pack

このコンテナ内でwasm-pack buildはできるものの、wasm-pack packは下記のエラーが発生した。

Error: Packaging up your code failed
Caused by: No such file or directory (os error 2)

対応

公式ドキュメントや Web 上を検索しても情報がなく困ったが、npm packを内部で呼び出しているのかと考え、下記コマンドを利用できるようにしたところ、無事wasm-pack packにて npm パッケージを作成することができた。

  • node
  • npm
  • npx

npmとサブコマンドが似ていることから当然といえばそうかもしれないが、気づかずに結構時間を使ってしまった。

なお、aptで入るものはバージョンが古いため、(差分が大きいが)下記コミットのようにアーカイブを展開する形で対応した。

update: devcontainer configuration (ff650765c668002ea1b8e8f056ef8a4907b6a6f8)

Node.js に関する該当箇所は下記

FROM busybox:1.34.1 as nodejs
ENV NODEJS_TARBALL_URL="https://nodejs.org/dist/v18.12.1/node-v18.12.1-linux-x64.tar.xz"
USER root
WORKDIR /root/.local
RUN mkdir -p bin/ node/ \
&& wget -O node.tar.xz "${NODEJS_TARBALL_URL}" --no-check-certificate \
&& xz -d -c node.tar.xz | tar xvf - -C node --strip-components 1 \
&& ln -s "/root/.local/node/bin/node" bin/ \
&& ln -s "/root/.local/node/bin/npm" bin/ \
&& ln -s "/root/.local/node/bin/npx" bin/ \
&& rm node.tar.xz

· 約3分
ogumaru

概要

有効/無効の 2 つの状態を持つ要素を利用して、createContextで作成した状態を保持・切り替えさせたい。

Calcite Design System のCalciteSwitchを利用し、状態をクリックで変更しようとしたが、実際はクリックをしても見た目が有効なまま変わらなかった。

意図した動作としては、選択状態に応じて下の表のようになる。

スイッチの状態表示
有効青、右
無効白、左

環境

項目バージョン
@esri/calcite-components-react0.32.0
react18.2.0

原因・対策

onClickでイベントの処理をしつつchecked={context.isSetAttr}のようにcheckedプロパティで状態管理をすると適切に表示されなくなる。

checked=truecheckedであれば当然青になるが、checked=falseであっても表示が上の表の「有効」のものになる。

ドキュメントにはcheckedtrue, falseを持つとあるが、実際にはfalseだと意図した通りの表示にならない。

下記コードでは状態に関わらず「有効」のものが表示される。

(contextは上位コンポーネントより渡しており、isSetAttrboolean)

<CalciteSwitch
checked={context.isSetAttr}
onClick={(e) => {
const input = e.target as HTMLInputElement;
context.setIsSetAttr(input.checked);
}}
/>

正しくは下記のようにonCalciteSwitchChangeでイベントの処理をしつつ、checkedに状態を持たせない。

<CalciteSwitch
onCalciteSwitchChange={(e) => {
const input = e.target;
context.setIsSetAttr(input.checked);
}}
/>

Fires when the checked value has changed. Note: The event payload is deprecated, use the component's checked property instead.

Switch | Calcite Design System | ArcGIS Developers - (developers.arcgis.com)

これで意図した表示にはなるが、コンポーネント自体に状態を保持しておらず、複数箇所からの操作がある場合に若干不安がある。

· 約4分
ogumaru

概要

ArcGIS API for JavaScript (@arcgis/core)で MapView を利用する際、インスタンス化時に container プロパティにセレクタの文字列かHTMLElementを指定することで、そこに地図を表示することができる。

React を利用する都合上、静的な HTML ではなく、表示対象となる要素(React コンポーネント)が描画されたあと、そこに表示されるようにしたい。

実装は ES Modules および TypeScript を利用して行った。

結論

useEffect内で__esri.Accessor.set()からcontainerを指定する。

import { useEffect } from "react";
import MapView from "@arcgis/core/views/MapView";

// ここでは container は指定しない。
// レイヤの操作をする都合上、
// export する必要があるためsetup内でインスタンス化できない。
export const mapView = new MapView({
/** 略 **/
});

const setup = () => {
// 動的に container を指定
mapView.set("container", "viewDiv");
};

export const App = () => {
useEffect(setup, []);
return (
<>
<div id="viewDiv"></div>
</>
);
};

環境

項目内容
React18.2.0
ArcGIS API for JavaScript4.24.7
TypeScript4.8.4
webpack5.74.0

動的な配置

MapViewがインスタンス化されたときにcontainerに指定した際、この時点で該当の HTML 要素がない場合は地図が表示されない。

React を利用している場合、コンポーネントがマウントされたタイミングで描画されるように、useEffect内でMapViewをインスタンス化すれば問題なく表示ができる。

containerプロパティ

サンプルコードなどでは下記のようにコンストラクタでcontainerに HTML 要素のid文字列を指定することが多い。

const mapView = new MapView({
// (略)
container: "viewDiv",
});

このとき、containerプロパティの型は string | HTMLDivElementとなっているため、TypeScript の型エラーは出ない。

(ヒントには__esri.DOMContainerProperties.container?: string | HTMLDivElementとでるが、該当の型はドキュメントには見つからなかった)

MapView | API Reference | ArcGIS API for JavaScript 4.24 | ArcGIS Developers - (developers.arcgis.com)

一方、MapViewがインスタンス化された場合、containerプロパティはDOMContainer.containerを指してしまい、指定できるのはHTMLDivElementのみとなる。

DOMContainer | API Reference | ArcGIS API for JavaScript 4.24 | ArcGIS Developers - (developers.arcgis.com)

そのため下記コードでは型エラーが発生してしまう。

const mapView = new MapView(...);
mapView.container = "viewDiv"

Type 'string' is not assignable to type 'HTMLDivElement'.ts(2322)

MapViewではなく、これが継承している__esri.Accessorsetメソッドを持っており、これを利用することで動的にcontainerを指定することができた。

Accessor | API Reference | ArcGIS API for JavaScript 4.24 | ArcGIS Developers - (developers.arcgis.com)

· 約4分
ogumaru

概要

ウェブブラウザの File API を利用して、ファイルのドラッグ & ドロップを利用した機能を実装している際、ファイルの検証を行っているにもかかわらず、エラーが発生した。

必ず失敗するわけではなく、たまに成功するため、動作の確認を行った。

原因

ファイルドロップ時にその中身が変わるのは、Wayland 環境における現状の挙動らしかった。

参照:

挙動の確認

環境

環境バージョン
OSUbuntu 22.04.1 LTS
ChromiunChromium 106.0.5249.119 snap
FirefoxMozilla Firefox 105.0.3
FilesGNOME nautilus 42.2

挙動の詳細と確認

デバッガを利用して確認を行った。

下記のようなコードにおいて、csv.getAsFile()nullになるために例外が発生していた。

体感として失敗する割合のほうが多いように感じる。

再現コード
// CSV形式かどうかの確認
const isCSVItem = (item) => {
const type = item.type;
const isCSV = [
type === "text/plain",
type === "text/csv",
type === "application/vnd.ms-excel",
type === "application/octet-stream",
].some((result) => result === true);
return isCSV;
};

document.body.addEventListener("dragover", (event) => {
event.stopPropagation();
event.preventDefault();
if (!event.dataTransfer) return;
event.dataTransfer.dropEffect = "copy";
});

document.body.addEventListener("drop", (event) => {
event.stopPropagation();
event.preventDefault();
if (!event.dataTransfer) return;

// item の確認
// 同じ操作でも異なる結果になる
for (const item of event.dataTransfer.items) {
console.log(item);
}

Array.from(event.dataTransfer.items)
.filter((item) => isCSVItem(item))
// ここで返り値が null になる
.map((csv) => csv.getAsFile())
.map((file) => {
if (!file) throw new Error("Failed to load csv file.");
const reader = new FileReader();
// file が たまに null になるため例外が発生する
reader.readAsDataURL(file);
reader.addEventListener("load", (event) => {
// ファイルに対する処理
});
});
});

成功する場合と失敗する場合を比較すると、上記コードのevent.dataTransfer.itemsの要素(DataTransferItem)の中身に違いがあり、下記 2 通りになっていることがわかった。

処理kindtype
成功filetext/csv
失敗stringtext/plain

Firefox ではこの問題は発生せず、kind: fileなものが取得できるため問題なく動作した。

対策

  1. ドロップされたデータのkindfileかどうか判定する
  2. input要素からファイルを選択して取得する
    input[type="file"]ではこの問題は発生しない
  3. ログイン時に [Ubuntu on Xorg] を利用する

今回は 1, 2 の対策を行って実装を行った。

· 約7分
ogumaru

概要

また beta がついているが、Ubuntu Pro が個人でも 5 台まで無償利用できるようになったため、早速利用してみる。

全体の流れとしては以下のようになる。

  1. Ubuntu Oneのアカウントの作成
  2. Ubuntu Proへログイン
  3. トークンの取得
  4. proコマンドを利用できるようにする
  5. 端末をアタッチ

Ubuntu Pro beta tutorialを参考に進めれば特に詰まるところもない。

環境

項目内容
OSUbuntu 22.04.1 LTS

利用手順

Ubuntu One アカウントの作成 / Ubuntu Pro へログイン

Ubuntu One のアカウントを作成したあと、Ubuntu Pro のページから再度ログインを行うと下図の画面になる。

Personal Data Request

なお、「Service authorization for Ubuntu.com」はチェックを入れないと進めなかった。

トークンの取得

Ubuntu Pro にログイン後、下記の画面から「UA subscriptions」を押下するとトークンが確認できる。

UA Subscriptions

Tokenとある箇所にトークン、その下にはアタッチの際のコマンドが書いてある。

Your subscriptions

proコマンドを利用できるようにする

詳細は後述するが、proコマンドのために追加でインストールが必要にはならなかった。

sudo apt update && sudo apt upgrade

端末のアタッチ

sudo pro attach "${表示されているTOKEN}"
# > Enabling default service esm-infra
# > Updating package lists
# > Ubuntu Pro: ESM Infra enabled
# > Enabling default service livepatch
# > Installing canonical-livepatch snap
# > Canonical livepatch enabled.
# > Unable to determine current instance-id
# > This machine is now attached to 'Ubuntu Pro - free personal subscription'
# >
# > SERVICE ENTITLED STATUS DESCRIPTION
# > esm-infra yes enabled Expanded Security # > Maintenance for Infrastructure
# > livepatch yes enabled Canonical Livepatch service
# >
# > NOTICES
# > Operation in progress: pro attach
# >
# > Enable services with: pro enable <service>
# >
# > Account: ogumaru@example.com
# > Subscription: Ubuntu Pro - free personal subscription

実行もほとんど時間はかからず、特に再起動を求められることはなかった。

上記コマンドでアタッチ後にはトップバーに下図の常駐アイコンが表示されるようになる。

Livepatchのアイコン

「Livepatch Settings」を押下するとライブパッチの設定のほか、トップバーの常駐アイコン表示切り替えや端末のデタッチもできる。

Detach this machine

トークンについて

Tokenは端末認証後も変わらず(=5 台とも同じトークンを利用するはず)、またトークンの更新画面も見つからなかった。

トークンが漏れてしまった場合の対策は気になる。

proコマンドの実体について

結論

ubuntu-advantageが実体となっている。

proコマンドの有無

これまでの環境ではproコマンドは利用できなかった。

# pro コマンドは利用できない
command -v pro

# 実行してもパッケージの候補にはでてこない
pro
# > Command 'pro' not found, did you mean:
# > command 'gpro' from snap gpro (1.0.24)
# > command 'ro' from deb golang-redoctober (0.0~git20161017.0.78e9720-5)
# > command 'proj' from deb proj-bin (8.2.1-1)
# > command 'prt' from deb prt (0.22-1)
# > command 'pio' from deb platformio (4.3.4-2)
# > command 'pr' from deb coreutils (8.32-4.1ubuntu1)
# > command 'pry' from deb pry (0.13.1-2)
# > See 'snap info <snapname>' for additional versions.

apt upgradeでパッケージ更新をしたあと利用できるようになる。

パッケージとしてはubuntu-advantage-toolsの一部なようで、これがアップデートされることでproが利用できるようになるようだ。

sudo apt update && sudo apt upgrade
# > ...
# > The following packages will be upgraded:
# > ubuntu-advantage-tools
# > 1 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
# > Need to get 163 kB of archives.
# > After this operation, 113 kB of additional disk space will be # used.
# > Do you want to continue? [Y/n] y
# > Get:1 http://jp.archive.ubuntu.com/ubuntu jammy-updates/main # amd64 ubuntu-advantage-tools amd64 27.11.2~22.04.1 [163 kB]
# > Fetched 163 kB in 0s (723 kB/s)
# > Preconfiguring packages ...
# > (Reading database ... 183922 files and directories currently installed.)
# > Preparing to unpack .../ubuntu-advantage-tools_27.11.2~22.04.1_amd64.deb ...
# > Unpacking ubuntu-advantage-tools (27.11.2~22.04.1) over (27.10.1~22.04.1) ...
# > Setting up ubuntu-advantage-tools (27.11.2~22.04.1) ...
# > Installing new version of config file /etc/ubuntu-advantage/help_data.yaml ...
# > Installing new version of config file /etc/ubuntu-advantage/uaclient.conf ...
# > Processing triggers for man-db (2.10.2-1) ...

実体の確認と関連コマンド

proコマンドの実体を確認するとubuntu-advantageへのシンボリックリンクになっていた。

command -v pro
# > /usr/bin/pro

file "$(command -v pro)"
# > /usr/bin/pro: symbolic link to ubuntu-advantage

また、ここに記載されているようなuaコマンドも、同様にubuntu-advantageへのシンボリックリンクとなっていた。

file "$(command -v ua )"
/usr/bin/ua: symbolic link to ubuntu-advantage

Ubuntu Pro Clientにも下記の記載がある。

Note: The Ubuntu Advantage client or UA client has been renamed to the Ubuntu Pro client in line with the rebranding of Ubuntu Advantage to Ubuntu Pro 4. Specific commands have also been updated to refer to Ubuntu Pro rather than Ubuntu Advantage.

ubuntu-advantage自体を呼び出しても、ヘルプ内はproとなっていた。

ubuntu-advantage --help
# > usage: pro <command> [flags]
# > ...

ドキュメントの修正コミット(docs: ua -> pro)でも見られるように、ドキュメントもproへ更新されている。

· 約6分
ogumaru

概要

これまで Wi-Fi 接続用の USB ドングルを利用する際にドライバのビルドが必要になることがあったため、作業の記録も兼ねてまとめておく。

依存パッケージが多いため、Docker を利用してビルドを行うこととする。

環境・利用機器

環境バージョン
OSUbuntu 22.04.1 LTS
Docker20.10.14, build a224086
製品コントローラ
アイ・オー・データ社 WNPU583Brtl8821ce
アイ・オー・データ社 WN-AC867Urtl8812au

WNPU583B は Wi-Fi/Bluetooth 両方使用できることを確認する。

ドライバのビルド・インストール

Docker コンテナの起動 (ホスト側)

rtl8812au のドライバが多く迷うが、GitHub での「rtl8812au」の検索結果の上位のものを利用した。

git clone https://github.com/gnab/rtl8812au.git driver
docker run --rm -it -v "$(pwd)/driver:/data" -w "/data" ubuntu:jammy /bin/bash

後述するカーネルヘッダのパッケージをインストールするためには、ホストとコンテナの OS(ディストリビューション)を合わせる必要がある。

最初gccのコンテナでビルドしようとしたが、ベースとなる OS が異なるためこのパッケージがなかった。

依存パッケージのインストール (コンテナ側)

apt update
apt install build-essential "linux-headers-$(uname -r)"

なお、linux-headers-*のパッケージをインストールしないと下記のエラーが出る。

make[1]: *** /lib/modules/5.15.0-47-generic/build: No such file or directory.

ドライバのビルド (コンテナ側)

make

ドライバのテスト (ホスト側)

sudo insmod driver/8812au.ko

この状態では再起動の際にモジュールへの参照が解除されてしまう。

インストール (ホスト側)

sudo cp 8812au.ko "/lib/modules/$(uname -r)/kernel/drivers/net/wireless"
sudo depmod

上記のほか、カーネルアップデートをした際に自動でビルドするには DKMS を利用する必要がある。

詳細はリポジトリの READMEに書いてある。

build-essentialdkmsなどのパッケージを入れる必要があるため、今回は対応しないこととする。

(別の機会に Docker を利用した方法を試したい。)

ソースコードの確認

ここまででドングルは動作する状態にはなったが、製品への対応状況を確認するため、今回利用したリポジトリと APT リポジトリのソースコードを比較しつつ確認してみる。

ベンダー ID・プロダクト ID の確認

ソースコードを探るにあたり、製品の ID を確認する。

lsusb | grep -i wn
# > Bus 001 Device 013: ID 0bda:0823 Realtek Semiconductor Corp. WNPU583B
# > Bus 001 Device 012: ID 04bb:0952 I-O Data Device, Inc. WN-AC867U

0bda:082304bb:0952がベンダー ID(idVendor)とプロダクト ID(idProduct)の値のため、これを利用してソースコードを検索する。 ベンダー ID を含めるとヒット件数が多いため、プロダクト ID を利用して検索を行う。

今回利用したリポジトリのソースコード

https://github.com/gnab/rtl8812au.gitのソースコード内から関連する記述を確認する。

git grep -iP '0x(0823|0952)'
# > os_dep/linux/usb_intf.c: {USB_DEVICE(0x04BB, 0x0952),.driver_info = RTL8812}, /* I-O DATA - Edimax */
# > os_dep/linux/usb_intf.c: {USB_DEVICE(0x0bda, 0x0823),.driver_info = RTL8821}, /* I-O DATA - WNPU583B */

APT リポジトリのソースコード

ホスト環境の設定を変えたくなかったのでコンテナ環境でソースコードを取得する

# 何かしらエディタをインストール
apt install busybox

# deb-src のコメントアウトを解除する
busybox vi /etc/apt/sources.list
apt update

# 関連するパッケージを検索
apt search --names-only 'rtl88[21]{2}'
# > Sorting... Done
# > Full Text Search... Done
# > rtl8812au-dkms/jammy,jammy 4.3.8.12175.20140902+dfsg-0ubuntu15 all
# > dkms source for the r8812au network driver
# >
# > rtl8821ce-dkms/jammy,jammy 5.5.2.1-0ubuntu10 all
# > DKMS source for the Realtek 8821C PCIe Wi-Fi driver

上記のパッケージ検索結果からrtl8812au-dkmsrtl8821ce-dkmsを確認することにする。

# ソースコードのダウンロード
apt source rtl8812au-dkms

下記のメッセージが出てきたが、今回はapt sourceでダウンロードしたものを確認する。

Picking 'rtl8812au' as source package instead of 'rtl8812au-dkms'
NOTICE: 'rtl8812au' packaging is maintained in the 'Git' version control system at:
https://github.com/rsalveti/rtl8812au.git
Please use:
git clone https://github.com/rsalveti/rtl8812au.git
to retrieve the latest (possibly unreleased) updates to the package.

apt source rtl8821ce-dkms

下記のメッセージが出てきたが、こちらもapt sourceでダウンロードしたものを確認する。

Picking 'rtl8821ce' as source package instead of 'rtl8821ce-dkms'
NOTICE: 'rtl8821ce' packaging is maintained in the 'Git' version control system at:
https://git.launchpad.net/~canonical-hwe-team/ubuntu/+source/rtl8821ce-dkms/+git/rtl8821ce-dkms
Please use:
git clone https://git.launchpad.net/~canonical-hwe-team/ubuntu/+source/rtl8821ce-dkms/+git/rtl8821ce-dkms
to retrieve the latest (possibly unreleased) updates to the package.

ダウンロードしたソースコードから関連する記述を検索する。

# ダウンロードされたファイルの確認
ls -1
# > rtl8812au-4.3.8.12175.20140902+dfsg
# > rtl8812au_4.3.8.12175.20140902+dfsg-0ubuntu15.debian.tar.xz
# > rtl8812au_4.3.8.12175.20140902+dfsg-0ubuntu15.dsc
# > rtl8812au_4.3.8.12175.20140902+dfsg.orig.tar.gz
# > rtl8821ce-5.5.2.1
# > rtl8821ce_5.5.2.1-0ubuntu10.debian.tar.xz
# > rtl8821ce_5.5.2.1-0ubuntu10.dsc
# > rtl8821ce_5.5.2.1.orig.tar.gz

# 関連する記述を確認
find -name "*.c" -print0 | xargs -0 grep -iP '0x(0823|0952)'
# > ./rtl8812au-4.3.8.12175.20140902+dfsg/.pc/0009-usb_intf-extending-compatible-vendor-list.patch/os_dep/linux/usb_intf.c: {USB_DEVICE(0x04BB, 0x0952),.driver_info = RTL8812}, /* I-O DATA - Edimax */
# > ./rtl8812au-4.3.8.12175.20140902+dfsg/.pc/0004-Adding-additional-compatible-devices.patch/os_dep/linux/usb_intf.c: {USB_DEVICE(0x04BB, 0x0952),.driver_info = RTL8812}, /* I-O DATA - Edimax */
# > ./rtl8812au-4.3.8.12175.20140902+dfsg/os_dep/linux/usb_intf.c: {USB_DEVICE(0x04BB, 0x0952),.driver_info = RTL8812}, /* I-O DATA - Edimax *

依存パッケージが多く入れたくないため確認していないが、上記からの予想として、WN-AC867U であればrtl8812auのパッケージをインストールすれば利用できる可能性がある。

WNPU583B に関連する部分はヒットしないため動作するか不明。

(過去の経験では Kubuntu 20.04 において rtl8821ce-dkmsをインストールすることで Bluetooth だけは利用できた。)

· 約5分
ogumaru

概要

Raspberry Pi 4 を利用して LAN 内に Samba サーバを構築し、iPhone に標準でインストールされている「ファイル」アプリからの接続を行う

環境

項目内容
クライアント機種iPhone 8 Plus
クライアント OSiOS 15.6.1
サーバ機種Raspberry Pi 4 Model B

Raspberry Pi OS はバージョンがないらしいため下記で確認

(ベータ版で出た 64-bit の OS を入れた記憶がある)

uname -a
# > Linux raspberrypi 5.10.103-v8+ #1529 SMP PREEMPT Tue Mar 8 12:26:46 GMT 2022 aarch64 GNU/Linux
lsb_release -a
# > No LSB modules are available.
# > Distributor ID: Debian
# > Description: Debian GNU/Linux 10 (buster)
# > Release: 10
# > Codename: buster
samba --version
# > Version 4.9.5-Debian

準備

Samba パッケージ

sudo apt update
sudo apt install samba
# iOS や macOSでも利用する場合
sudo apt install samba-vfs-modules

ユーザ

ここはSamba - ArchWiki (wiki.archlinux.jp)を参考に設定した

# Sambaユーザの追加
sudo pdbedit -a "$(whoami)"

# `usershare path` で指定する`/var/lib/samba/usershares` にアクセスするため
# (所有者:グループ が root:sambashare になっている)
sudo gpasswd sambashare -a "$(whoami)"

グループ設定を反映させるため再ログインする

ファイアウォール

Firewalling Samba (www.samba.org)によると、許可するのは下記

プロトコルポート
UDP137
UDP138
TCP139
TCP445

ufwを利用していれば下記

# Samba のルールがあれば利用
sudo ufw app list | grep -i samba
sudo ufw allow samba

Samba の設定

グローバル設定ファイル

# 設定ファイルのバックアップ
sudo cp -a /etc/samba/smb.conf{,.$(date '+%Y-%m-%d_%H%M%S').bak}

# 下記差分を追記
sudoedit /etc/samba/smb.conf
[global]
+ usershare path = /var/lib/samba/usershares
+ # For supporting iOS / macOS
+ vfs objects = fruit streams_xattr
+ fruit:metadata = stream
+ fruit:model = MacSamba
+ fruit:posix_rename = yes
+ fruit:veto_appledouble = no
+ fruit:nfs_aces = no
+ fruit:wipe_intentionally_left_blank_rfork = yes
+ fruit:delete_empty_adfiles = yes

上記のvfsfruitの設定がないと、保存の際に「操作を完了できませんでした」「属性が見つかりません」のようなエラーメッセージが表示される

VFS 関連の設定内容については下記ページのものを利用した

Configure Samba to Work Better with Mac OS X - SambaWiki (wiki.samba.org)

共有設定ファイル

# 共有するディレクトリの作成
mkdir -p ~/share

# 共有設定ファイルの作成
net usershare add "share" ~/share/ "samba share" "$(whoami):f"

権限の設定については net (www.samba.org)などを参考に指定する

The definition of a user defined share acl is: "user:permission", where user is a valid username on the system and permission can be "F", "R", or "D". "F" stands for "full permissions", ie. read and write permissions. "D" stands for "deny" for a user, ie. prevent this user from accessing this share. "R" stands for "read only", ie. only allow read access to this share (no creation of new files or directories or writing to files).

設定の反映

sudo systemctl reload smbd.service

接続

mDNS で名前解決ができれば、同じネットワークにつないだ上で iOS の「ファイル」から

  1. [ブラウズ]
  2. 右上の丸で囲まれた三点(…)
  3. [サーバへ接続]
  4. raspberrypi.localなど
  5. [登録ユーザ] を選択
  6. Raspberry Pi のユーザ情報を入力

で接続できる

例えば写真は共有から「"ファイル"に保存」から上記フォルダを選択することで保存できる

当初 VFS 関連(vfsfruit)を設定しておらず、ここで保存ができずに困ってかなり時間を使った

パフォーマンスについて

1MB の画像をコピーするのに 体感で 1 秒程度

10MB の動画をコピーするのに 体感で 2 〜 3 秒程度

SSD を接続した USB ブートで利用しているため、Raspberry Pi 環境の中では比較的 I/O は早いはず

microSD 環境ではもう少し遅いかもしれない