| 1 | # Little Beast |
| 2 | シンプル、実践的、アンチ・ブロート |
| 3 | |
| 4 |  |
| 5 | [](https://ghloc.vercel.app/TechnicalSuwako/LittleBeast?filter=.php%24%2C.maron%24%2C.css%24) |
| 6 | |
| 7 | サンプル:[https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/) |
| 8 | |
| 9 | ## Little Beast とは? |
| 10 | Little Beast は PHP 8.5 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\ |
| 11 | メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\ |
| 12 | 各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\ |
| 13 | 全てのモジュールはゼロから書かれており、データベースは一切必要ありません。 |
| 14 | |
| 15 | Little Beast はテクニカル諏訪子がゲーム開発に完全復帰する前の最後の Web プロジェクトです。 |
| 16 | |
| 17 | ## Little Beast が「ではない」物 |
| 18 | * 汎用フレームワーク |
| 19 | * 使い憎い |
| 20 | * Web 開発者向け |
| 21 | * 万人向け |
| 22 | * インストールが面倒 |
| 23 | * 教条的 |
| 24 | * 民主的に運営される |
| 25 | |
| 26 | ## Little Beast が「持っていない」物 |
| 27 | * データベース(全てファイルベース) |
| 28 | * 依存関係 |
| 29 | * パッケージマネージャー |
| 30 | * Docker/Kubernetes/Vagrant/Nix 等のコンテナ |
| 31 | * JavaScript |
| 32 | * ブロートウェア |
| 33 | * ORM |
| 34 | * キューやバックグラウンドジョブ |
| 35 | * クラウド/サーバーレス統合 |
| 36 | * スキャフォールディング |
| 37 | * コード生成 |
| 38 | * 抽象化レイヤー |
| 39 | * 不要なファイル |
| 40 | * DEI、行動規範、その他の差別的慣行 |
| 41 | |
| 42 | ## 独自機能 |
| 43 | * データベース不要 |
| 44 | * Composer や PEAR 不要 |
| 45 | * サーバーオーバーヘッドゼロ |
| 46 | * コンテナ不要 |
| 47 | * Maron テンプレートエンジン |
| 48 | * カスタム Markdown |
| 49 | * ActivityPub |
| 50 | * 画像ライブラリ(PNG・APNG・BMP・TGA・GIF・JPEG・WebP の読み書き。GD 等の拡張は不要) |
| 51 | * 二段階認証(認証アプリのワンタイムパスワード、QR コード) |
| 52 | * 多言語対応 |
| 53 | * マルチブログ対応 |
| 54 | * 100% 綺麗で正しい HTML5 と CSS3 |
| 55 | * 100% 正しい PHP |
| 56 | * SEO フレンドリー |
| 57 | * モジュラー CSS |
| 58 | * Atom フィード |
| 59 | * 組み込みテストスイート |
| 60 | |
| 61 | ## インストール方法 |
| 62 | ```sh |
| 63 | cd /var/www/htdocs |
| 64 | git clone https://github.com/TechnicalSuwako/LittleBeast.git . |
| 65 | mv config/config.sample.php config/config.php |
| 66 | ``` |
| 67 | |
| 68 | HTTP サーバーを設定して `/public` をルートとして `php-fpm` で実行して下さい。\ |
| 69 | それだけです! |
| 70 | |
| 71 | ### OpenBSD サーバー |
| 72 | ```sh |
| 73 | pkg_add php-8.5.5 php-gmp-8.5.5 |
| 74 | rcctl enable php85_fpm httpd relayd |
| 75 | rcctl start php85_fpm httpd relayd |
| 76 | ``` |
| 77 | |
| 78 | #### httpd |
| 79 | ``` |
| 80 | server "technicalsuwako.moe" { |
| 81 | listen on * tls port 8443 |
| 82 | gzip-static |
| 83 | tls { |
| 84 | certificate "/etc/ssl/technicalsuwako.moe.crt" |
| 85 | key "/etc/ssl/private/technicalsuwako.moe.key" |
| 86 | } |
| 87 | root "/htdocs/technicalsuwako.moe/www/public" |
| 88 | directory index "index.php" |
| 89 | location "/.well-known/acme-challenge/*" { |
| 90 | root "/acme" |
| 91 | request strip 2 |
| 92 | } |
| 93 | |
| 94 | location "/*.php" { |
| 95 | fastcgi socket "/run/php-fpm.sock" |
| 96 | } |
| 97 | |
| 98 | location "/*.php[/?]*" { |
| 99 | fastcgi socket "/run/php-fpm.sock" |
| 100 | } |
| 101 | |
| 102 | location "/" { |
| 103 | directory index "index.php" |
| 104 | } |
| 105 | location match "/blog/" { |
| 106 | request rewrite "/index.php" |
| 107 | } |
| 108 | location match "/about" { |
| 109 | request rewrite "/index.php" |
| 110 | } |
| 111 | location match "/monero" { |
| 112 | request rewrite "/index.php" |
| 113 | } |
| 114 | location match "/secret" { |
| 115 | request rewrite "/index.php" |
| 116 | } |
| 117 | location match "/ap/" { |
| 118 | request rewrite "/index.php" |
| 119 | } |
| 120 | location "/.well-known/webfinger" { |
| 121 | request rewrite "/index.php" |
| 122 | } |
| 123 | location "*.atom" { |
| 124 | request rewrite "/index.php" |
| 125 | } |
| 126 | location match "/comments" { |
| 127 | request rewrite "/index.php" |
| 128 | } |
| 129 | location "/notifications" { |
| 130 | request rewrite "/index.php" |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | server "www.technicalsuwako.moe" { |
| 135 | listen on * tls port 8443 |
| 136 | gzip-static |
| 137 | tls { |
| 138 | certificate "/etc/ssl/technicalsuwako.moe.crt" |
| 139 | key "/etc/ssl/private/technicalsuwako.moe.key" |
| 140 | } |
| 141 | block return 301 "https://technicalsuwako.moe$REQUEST_URI" |
| 142 | } |
| 143 | ``` |
| 144 | |
| 145 | #### relayd |
| 146 | ``` |
| 147 | relayd_addr="0.0.0.0" |
| 148 | router_addr="192.168.10.106" |
| 149 | |
| 150 | table <httpd> { $router_addr } |
| 151 | |
| 152 | http protocol reverse { |
| 153 | tcp { nodelay, sack, socket buffer 65536, backlog 100 } |
| 154 | tls ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256" |
| 155 | tls keypair "technicalsuwako.moe" |
| 156 | |
| 157 | return error |
| 158 | |
| 159 | match request header append "X-Forwarded-For" value "$REMOTE_ADDR" |
| 160 | match request header append "X-Forwarded-Port" value "$REMOTE_PORT" |
| 161 | |
| 162 | #match response header set "Referrer-Policy" value "same-origin" |
| 163 | match response header set "X-Frame-Options" value "deny" |
| 164 | match response header set "X-Content-Type-Options" value "nosniff" |
| 165 | match response header set "Referrer-Policy" value "strict-origin-when-cross-origin" |
| 166 | match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload" |
| 167 | match response header set "Cross-Origin-Opener-Policy" value "same-origin" |
| 168 | match response header set "Content-Security-Policy" value "img-src 'self' https://*.076.moe http://*.076.moe https://*.technicalsuwako.moe http://*.technicalsuwako.moe; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; object-src 'none';" |
| 169 | match response header append "Permissions-Policy" value "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=()" |
| 170 | |
| 171 | match request header "Accept-Encoding" value "gzip" tag "gzip" |
| 172 | match response header "Content-Type" value "text/*" tag "compress" |
| 173 | match response header "Content-Type" value "text/html" tag "charset=UTF-8" |
| 174 | match response header "Content-Type" value "application/javascript" tag "compress" |
| 175 | match response header "Content-Type" value "application/json" tag "compress" |
| 176 | |
| 177 | pass request quick header "Host" value "technicalsuwako.moe" forward to <httpd> |
| 178 | |
| 179 | pass |
| 180 | } |
| 181 | |
| 182 | relay www_tls { |
| 183 | listen on $relayd_addr port 443 tls |
| 184 | protocol reverse |
| 185 | |
| 186 | # Default |
| 187 | forward to <httpd> port 8443 check tcp |
| 188 | } |
| 189 | |
| 190 | relay www_www { |
| 191 | listen on $relayd_addr port 80 |
| 192 | protocol reverse |
| 193 | |
| 194 | # Default |
| 195 | forward to <httpd> port 8080 check tcp |
| 196 | } |
| 197 | ``` |
| 198 | |
| 199 | ### ブログと Atom フィード |
| 200 | 1つのサイトに複数のブログを置けます。`config/config.php` の `BLOGS` の各ディレクトリ(例:`blog`、`enblog`)がブログになり、それぞれ `/ディレクトリ.atom` の Atom フィードがあります(一覧と検索結果のページの上にリンクがあります)。\ |
| 201 | `BLOGS` が無ければ、今まで通り `blog` だけです。 |
| 202 | |
| 203 | ### 静的ファイル(ASS_DOMAIN) |
| 204 | `config/config.php` の `ASS_DOMAIN` で静的ファイル(画像、CSS、アイコン、アップロードしたファイル)の場所を決めます。無ければ `/static` です。 |
| 205 | * パス(例:`/static`)なら、このサイトの `public/` の下のディレクトリです(`/static` は `public/static/`)。 |
| 206 | * URL(例:`https://ass.example.com/static`)なら、別のサーバー(サブドメイン等)です。アップロードしたファイルは今まで通り `public/` の下の同じパス(`public/static/`)に保存するので、そのディレクトリを静的ファイルのサーバーと同期して下さい(又はそのサーバーから配信して下さい)。 |
| 207 | |
| 208 | テンプレートやコードでは、URLは `ass_url('article/a.png')`、ディスク上のファイルは `ROOT.ass_path('article/a.png')` を使います。 |
| 209 | |
| 210 | ### データベース(MySQL・MariaDB、PostgreSQL、SQLite) |
| 211 | Little Beast 自体はデータベースを使いませんが、必要な場合(既存のアプリの移植等)は `Std\Lib\Db` で繋げます。ドライバは PHP だけで書かれているので、mysqli・pdo_mysql・pgsql・pdo_pgsql・sqlite3・pdo_sqlite・sockets は不要です。\ |
| 212 | `config/config.php` で `DB_ENABLED` を `true` にして、`DBINFO` を設定して下さい(`driver` は `mysql`(MariaDB も同じ)、PostgreSQL の `pgsql`(ポートは普通5432)、又は `sqlite`(`path` にデータベースのファイル、相対パスはサイトのルートから、例:`data/site.sqlite`)。`socket` を指定すると `host`・`port` の代わりに UNIX ソケットを使う、PostgreSQL ではソケットのファイル又は `.s.PGSQL.5432` が有るディレクトリ(例:`/tmp`)。`tls` は `true`、又は `['cafile' => '/etc/ssl/ca.pem']` 等の SSL コンテキストオプションの配列)。 |
| 213 | ```php |
| 214 | $db = new \Std\Lib\Db(); |
| 215 | $rows = $db->query('SELECT * FROM users WHERE id = ?', [1])['rows']; |
| 216 | $id = $db->query('INSERT INTO users (name) VALUES (?)', ['諏訪子'])['insertId']; // MySQL・SQLite |
| 217 | $id = $db->query('INSERT INTO users (name) VALUES (?) RETURNING id', ['諏訪子'])['rows'][0]['id']; // PostgreSQL |
| 218 | $db->transaction(function(\Std\Lib\Db $db): void { |
| 219 | $db->query('UPDATE users SET points = points - ? WHERE id = ?', [100, 1]); |
| 220 | $db->query('UPDATE users SET points = points + ? WHERE id = ?', [100, 2]); |
| 221 | }); |
| 222 | ``` |
| 223 | * 最初のクエリで接続します。パラメータ付きのクエリはプリペアドステートメントで実行するので、値をエスケープする必要はありません。 |
| 224 | * 整数の列は `int`、`FLOAT`・`DOUBLE`(PostgreSQL は `real`・`double precision`)は `float`、`NULL` は `null`、その他(`DECIMAL`・`numeric`、日付、JSON、`BLOB` 等)は文字列で返します。PostgreSQL の `boolean` は `bool`、`bytea` は生のバイト、配列は PHP の配列で返します。 |
| 225 | * エラーは `Std\Lib\Db\DbException`(`getCode()` は MySQL のエラーコード、例:重複は1062、`sqlState` は SQLSTATE、例:PostgreSQL の重複は23505)を投げ、`log/db_log.txt` に書きます。 |
| 226 | * MySQL の認証:`mysql_native_password`、`caching_sha2_password`(MySQL 8 の既定)と `sha256_password`(TLS でない TCP では、パスワードをサーバーの RSA の鍵で暗号化する)、`mysql_clear_password`(TLS か UNIX ソケットのみ)。MariaDB の `client_ed25519`、圧縮、`LOAD DATA LOCAL INFILE` は未対応です。 |
| 227 | * SQLite:`Std\Lib\Db\Sqlite` は SQLite 3 のファイルを自分で読み書きするので(ロールバックジャーナル、UTF-8)、`sqlite3` のコマンド等でも開けます。途中で止まった書き込みは、次に開いた時にどちらでも元に戻ります。普通の SQL に対応しています:テーブル・インデックス・ビュー、`ALTER TABLE`(名前の変更、列の追加・削除・名前の変更)、`INSERT`(`OR REPLACE`・`OR IGNORE`、`ON CONFLICT`、`RETURNING`)、`UPDATE … FROM`、結合、`GROUP BY` と集約、サブクエリ、`UNION`・`INTERSECT`・`EXCEPT`、`WITH`、トランザクションとセーブポイント、普通の関数(文字列、数値、日付、JSON)、`PRAGMA`(`table_info`、`index_list`、`user_version`、`integrity_check` 等)。`=`・`IN`・範囲の検索にはインデックスを使います。WAL モード、トリガー、`WITHOUT ROWID`、仮想テーブル(FTS)、ウィンドウ関数、再帰の `WITH`、外部キーの確認、部分・式のインデックス、`ATTACH` は未対応です。ロックは `flock()` なので、サイトの動作中にファイルに書き込むのは PHP のプロセスだけにして下さい。プレースホルダは `?`・`?1`・`:name`。UTF-8 でない文字列は BLOB として保存します(`Std\Lib\Db\Sqlite\Blob` で明示も可)。 |
| 228 | * PostgreSQL の認証:SCRAM-SHA-256(TLS ならチャネルバインディング付きの SCRAM-SHA-256-PLUS、オプションの `channel_binding` は `prefer`・`require`・`disable`)、MD5、`password`(`allow_cleartext` を `true` にしない限り、TLS か UNIX ソケットのみ)、`trust`。GSSAPI・SSPI は未対応です。 |
| 229 | * PostgreSQL:`?` のプレースホルダは `$1`、`$2`… にします(文字列・識別子・コメントの外のみ。`?` そのもの(jsonb の演算子等)は `??` と書く)。`$1` もそのまま使えます。挿入した ID は無いので、`RETURNING` を使って下さい。トランザクションの中でエラーが起きた後の `COMMIT` は、PostgreSQL がロールバックするので `DbException`(SQLSTATE 25P02)を投げます。`COPY`、`LISTEN` の通知、実行中のクエリの取り消しは未対応です。PostgreSQL 16 で確認しました。 |
| 230 | * テストは偽物のサーバーを使います。PostgreSQL のテストを本物のサーバーでも実行するには、`LB_TEST_PGSQL=pgsql://ユーザー:パスワード@ホスト:ポート/データベース` を設定して下さい(TLS は `?cafile=/path/ca.pem` を追加)。名前が `lb_test_` で始まるテーブルを作って消します。 |
| 231 | |
| 232 | ### データベースの変換 |
| 233 | `Std\Lib\Db\Convert` は、MySQL・MariaDB、PostgreSQL、SQLite、SQL のダンプのファイル、`data/` の Little Beast の JSON のデータの間で、テーブルと行をどの方向にもコピーします。ドライバーと同じく PHP だけで書いてあります。 |
| 234 | ```php |
| 235 | use Std\Lib\Db; |
| 236 | use Std\Lib\Db\Convert; |
| 237 | use Std\Lib\Db\Convert\{DbSource, DbTarget, DumpSource, DumpTarget, JsonSource, JsonTarget}; |
| 238 | |
| 239 | // data/(JSON)→ SQLite |
| 240 | $report = Convert::run(new JsonSource(ROOT.'/data'), new DbTarget(new Db(['driver' => 'sqlite', 'path' => 'data/site.sqlite']))); |
| 241 | // PostgreSQL → MySQL のダンプのファイル |
| 242 | Convert::run(new DbSource(new Db(['driver' => 'pgsql', 'host' => '127.0.0.1', 'port' => 5432, 'username' => 'lb', 'password' => '…', 'dbname' => 'lb'])), |
| 243 | new DumpTarget('/tmp/lb.sql', 'mysql')); |
| 244 | // mysqldump・pg_dump のファイル → PostgreSQL(既に有るテーブルは置き換える) |
| 245 | Convert::run(new DumpSource('/tmp/dump.sql'), new DbTarget(new Db()), ['drop' => true]); |
| 246 | ``` |
| 247 | * 読む元:`DbSource`(接続した `Db`)、`DumpSource`(`mysqldump`、`pg_dump`(プレーンの形式、`COPY` 又は `--inserts`)、`sqlite3 .dump`、`DumpTarget` で作った `.sql` のファイル(`.sql.gz` は PHP の zlib が必要)、方言は自動で判定)、`JsonSource`(データのディレクトリ)。書く先:`DbTarget`、`DumpTarget`(方言は `mysql`・`pgsql`・`sqlite`、`mysql`・`psql`・`sqlite3` のコマンドで読み込めます)、`JsonTarget`。 |
| 248 | * オプション:`tables`(このテーブルだけ)、`drop`(書く先の同じ名前のテーブルを消す、無ければ既に有るテーブルはエラー)、`progress`(`fn(string $table, int $rows)`)。結果は `tables`(テーブル毎のコピーした行数)と `warnings`(変換しなかった物)。 |
| 249 | * 列、主キー、ユニーク・普通のインデックス、`NOT NULL`、既定値、自動の番号(`AUTO_INCREMENT`、`SERIAL`・IDENTITY、SQLite の `INTEGER PRIMARY KEY`)を変換し、番号はコピーした行の続きから始まります。型は書く先の一番近い型にします(例:`TINYINT(1)` と `boolean`、`JSON` と `jsonb`、`BLOB` と `bytea`)。`ENUM`・`SET` は文字列、PostgreSQL の配列は JSON にします。時差の有る日時は UTC で保存します。外部キー、ビュー、トリガー、関数、全文・式のインデックスは変換しません(`warnings` に出ます)。 |
| 250 | * JSON のデータ:ユーザー、コメント、通知、Cron のジョブ、フォロワー、ActivityPub の投稿は名前が `lb_` で始まるテーブルになり、他のファイル(`cache/`、`ratelimit/`、`tmp/`、ロック・一時ファイル、SQLite のデータベースを除く)は `lb_files` に入るので、戻すと同じファイルになります。他のテーブルは `data/db/{テーブル}.json` になります。`data/` が空・無い場合は、テーブルが無いだけです。`JsonTarget` は、2番目の引数(上書き)が `true` か `drop` が無ければ、空でないディレクトリには書きません。 |
| 251 | * サイトが使っているデータベースを読む・書く場合は、先に書き込みを止めて下さい(又は新しいデータベースに変換)。変換中に元をロックしません。 |
| 252 | |
| 253 | ### Git |
| 254 | `Std\Lib\Git` は Git のリポジトリを PHP だけで読み書きします(PHP の zlib が必要、`git` のコマンドは不要)。SHA-1 と SHA-256 のリポジトリに対応しています。サイトでリポジトリをホストする為の基礎です。 |
| 255 | ```php |
| 256 | use Std\Lib\Git\{Repository, Tree, Commit, Person, Diff, Blame}; |
| 257 | |
| 258 | $repo = Repository::init('/var/www/git/suwako/test.git', 'sha1', 'main', 'group'); // 又はRepository::open(...) |
| 259 | $me = Person::now('諏訪子', 'suwako@example.com'); |
| 260 | $tree = $repo->updateTree(null, ['README.md' => [Tree::FILE, "# テスト\n"]]); |
| 261 | $id = $repo->writeCommit(Commit::create($tree, [], $me, $me, "最初のコミット\n")); |
| 262 | $repo->refs->update('refs/heads/main', $id, $repo->zeroId()); // 途中でブランチが変わっていれば失敗 |
| 263 | |
| 264 | foreach ($repo->log('main', ['path' => 'README.md', 'limit' => 20]) as $c) echo $c->id, ' ', $c->subject(), "\n"; |
| 265 | echo $repo->blob($repo->resolve('main~1:README.md')); |
| 266 | echo Diff::unified($repo, 'main~1', 'main'); |
| 267 | $lines = Blame::file($repo, 'main', 'README.md'); |
| 268 | ``` |
| 269 | * 保存:ルーズオブジェクト、パック(`.idx` v1・v2 と `.rev`、差分も解決)、参照と `packed-refs`、`objects/info/alternates`、`core.sharedRepository`。`main~2`、`v1.0^{}`、`HEAD^2`、`main:パス/ファイル` 等のリビジョンが使えます。`Diff` は `git diff` と同じ出力(Myers)、`Blame` は同じ行が有る最初の親を辿り、`Maintenance::gc()` は到達出来るオブジェクトを1つのパックにまとめます(Cron で実行)。 |
| 270 | * ネットワーク:`Http::handle()` は Smart HTTP(`git clone https://…`、`git fetch`、`git push`)、`Ssh::handle()` は SSH の `git clone git@host:…` と `git push` を処理します。プロトコル v0・v1・v2、浅いクローン(`--depth`、`--deepen`)、`include-tag`、サイドバンドの進捗、atomic なプッシュ、受け取ったオブジェクトの確認に対応しています。送るパックはディスクの差分を再利用します。アクセスの確認は呼ぶ側で行い、`ReceivePack` には `check`・`update`・`after` のコールバックを渡せます(保護されたブランチ等)。 |
| 271 | ```php |
| 272 | // GET …/repo.git/info/refs、POST …/repo.git/git-upload-pack と git-receive-pack |
| 273 | Http::handle(Repository::open($dir), $action, ['push' => $userMayPush]); |
| 274 | |
| 275 | // SSH:gitユーザーのauthorized_keys |
| 276 | // command="/usr/local/bin/php-8.5 /var/www/htdocs/gitssh.php suwako",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA… |
| 277 | exit(Ssh::handle((string)getenv('SSH_ORIGINAL_COMMAND'), fn(string $path, bool $write) => /* アクセスの確認 */ Repository::open('/var/www/git/'.$path))); |
| 278 | ``` |
| 279 | * OpenBSD では php-fpm は `/var/www` に chroot されるので、リポジトリはその中に置き(例:`git` ユーザーのホームを `/var/www/git` に)、`$shared = 'group'` で作り、`www` を `git` グループに入れると、サイトと SSH の両方から書けます。SSH でプロトコル v2 を使うには、`sshd_config` に `AcceptEnv GIT_PROTOCOL` を追加して下さい。 |
| 280 | * 未対応:新しい差分の作成(ルーズで受け取ったオブジェクトは差分無しでパックにする)、シンパック、部分クローン(`--filter`)、`--shallow-since`・`--shallow-exclude`、`git archive --remote`、LFS、commit-graph・multi-pack-index の書き込み。 |
| 281 | |
| 282 | ### Gitler(Git のホスティング) |
| 283 | Gitler(`/gitler`)は上の Git のライブラリでサイトに Git のリポジトリをホストします:リポジトリのページ(ファイル、README、履歴、差分、blame、ブランチ、タグ、ZIP・TAR.GZ のダウンロード)、非公開のリポジトリ、共同作業者(読むだけ又は読み書き)、HTTPS と SSH のクローン・プッシュ。JavaScript は不要です。 |
| 284 | * `config/config.php` で `GITLER_ENABLED` を `true`(`AUTH_ENABLED` も必要)にして、`GITINFO` を設定して下さい:`root` はリポジトリのディレクトリ(php-fpm の chroot の中と外で同じ設定を使える様に、サイトのディレクトリからの相対パス。`/var/www/git` なら、サイトが `/var/www/htdocs` の場合は `../git`、`/var/www/htdocs/example.com/www` の場合は `../../../git`)、`ssh` はクローンの為に表示する SSH のアドレス(例:`git@example.com`、空なら HTTPS だけ)、`sshPort`、`php` は chroot の外の PHP のコマンド、`objectFormat` と `branch`(既定は `master`)は新しいリポジトリの形式とブランチ、`maxRepos` は1人の最大の数。 |
| 285 | * ロール:「Gitのリポジトリ」(自分のリポジトリを作る・管理する。管理者が付けるまで誰も持っていない)、「Gitのイシュー」(既定は全員)、「Gitのプルリクエスト」(既定は無し)。イシューとプルリクエストは今後。管理者は全部のリポジトリを見て変更出来ます。 |
| 286 | * HTTPS:`git clone https://example.com/gitler/user/repo.git`。公開のリポジトリはログイン無しでクローン出来ます。非公開のリポジトリとプッシュは、ユーザー名と `/gitler/-/keys` で作ったアクセストークン(読むだけ又は読み書き)を聞きます。二段階認証が無いユーザーはパスワードも使えます。プッシュ出来るのはブランチとタグだけです。OpenBSD の httpd が受け付けるリクエストの本文は既定で 1MB までなので、プッシュの為に `server` のブロックで増やして下さい(例:`connection max request body 536870912`)。 |
| 287 | * SSH:ユーザーは `/gitler/-/keys` で公開鍵を追加します。chroot の中にホームの有る `git` ユーザーを作り、`www` をそのグループに入れ、sshd が Gitler に鍵を聞く様にします: |
| 288 | ```sh |
| 289 | useradd -m -d /var/www/git -s /bin/sh git |
| 290 | usermod -G git www # php-fpmがリポジトリに書ける |
| 291 | usermod -G www git # gitler.phpがconfig/config.phpとdata/を読める |
| 292 | chmod 2775 /var/www/git |
| 293 | ``` |
| 294 | ``` |
| 295 | # /etc/ssh/sshd_config |
| 296 | Match User git |
| 297 | AuthorizedKeysCommand /usr/local/bin/php-8.5 /var/www/htdocs/gitler.php keys %u %t %k |
| 298 | AuthorizedKeysCommandUser git |
| 299 | AcceptEnv GIT_PROTOCOL |
| 300 | PasswordAuthentication no |
| 301 | ``` |
| 302 | 鍵毎に `gitler.php serve <ユーザー>` を実行するので、ユーザーが出来るのは `git clone`・`git push`(`git@example.com:user/repo.git`)だけです。`AuthorizedKeysCommand` を使わない場合は、`php gitler.php authorized-keys > /var/www/git/.ssh/authorized_keys`(例えば cron から)で同じ行を書けます。 |
| 303 | * リポジトリを整理する為に Cron のジョブ `gitlergc` を追加するか(例:`php cron.php add "@daily" gitlergc`)、`php gitler.php gc` を実行して下さい。 |
| 304 | * リポジトリのページには GitHub の様に言語の割合が出ます(バイト数で、ファイル名・拡張子・`#!` で決めます)。数えるのはプログラミングとマークアップの言語だけで、データ(JSON、YAML 等)、文章(Markdown 等)、外部のコード(`vendor/`、`node_modules/` 等)、ドキュメント(`docs/`、`examples/` 等)、生成されたファイルは数えません。ルートの `.gitattributes` の `linguist-language`、`linguist-vendored`、`linguist-generated`、`linguist-documentation`、`linguist-detectable` で変えられます(例:`*.h linguist-language=C++`)。結果はリポジトリの中の `gitler-languages.json` にツリー毎に保存し、一覧には既定のブランチの一番多い言語が出ます。 |
| 305 | * イシュー:ラベル、担当者、マイルストーン、Markdown の本文とコメント(「#3」は他のイシューへのリンク)。リポジトリを読めて「Gitのイシュー」のロールの有るユーザーが作ってコメント出来、書き込み権限の有る人がラベル等を付けて閉じます(作った人も閉じられます)。既定のブランチにプッシュしたコミットのメッセージの「fixes #3」「closes #3」「resolves #3」でイシューを閉じ、「#3」はイシューに記録します。設定でリポジトリ毎に無効に出来ます。データはリポジトリの中の `gitler/` です。 |
| 306 | * Fediverse(マルチユーザーの ActivityPub が有効な場合):公開のリポジトリは `@所有者.名前@ホスト` のアカウントになり、Mastodon 等からフォローするとイシューとコメントが届き、返信するとコメント、リポジトリをメンションした投稿は新しいイシューになります。ForgeFed の `Offer{Ticket}` と `Resolve` にも対応します(イシューの Ticket は `…/issues/3/ticket`)。リポジトリの設定でオフにしたり、アカウントやドメインをブロック出来ます(サイト全体のドメインのブロックも効きます)。配信には cron の `apdeliver` を使い、SSH のプッシュ(`git` ユーザー)で閉じたイシューは `gitler/spool/` に溜めて cron の `notify` が送ります。鍵の作成に GMP が必要です。 |
| 307 | * 通知:所有者と共同作業者は最初からリポジトリをウォッチし(新しいイシューと全部の動き)、他の人は作った・コメントした・担当の・メンション(`@ユーザー`)されたイシューの動きを受け取ります。リポジトリのページの「通知」でウォッチ・参加しているイシューだけ・無視を、イシューのページで受け取る・止めるを選べます。通知はサイトの「通知」に届き、`MAILER_ENABLED` ならメールでも届きます(`/gitler/-/keys` で止められます)。メールと SSH のプッシュの通知は cron の `notify` が送るので追加して下さい:`php cron.php add "* * * * *" notify`。 |
| 308 | * アーカイブ:リポジトリの設定でアーカイブすると読むだけになります(プッシュ、イシュー・コメント・ラベル・マイルストーンの変更、Fediverse からのコメントを止めます)。何時でも解除出来ます。 |
| 309 | * プルリクエスト(「Gitのプルリクエスト」のロールが必要):同じリポジトリのブランチ、フォーク(リポジトリのページの「フォークする」、「Gitのリポジトリ」のロールが必要)のブランチ、又はフォークしないで `git push origin HEAD:refs/for/master/トピック`(AGit、読めるだけのリポジトリにも。同じトピックにもう一度プッシュすると更新)から作れます。新しいブランチをプッシュすると、プルリクエストを作る URL が表示されます。会話、コミット、変更されたファイル(行番号を押すとその行にコメント、下書きをまとめてレビュー(コメント・承認・変更を依頼))が有り、書き込み権限の有る人がマージコミット・スカッシュ・リベース(出来れば fast-forward)でマージします(コンフリクトは PHP の3方向のマージで調べ、有ればローカルで解決)。本文の「fixes #3」でマージした時にイシューを閉じ、マージ先にプッシュで入ったプルリクエストはマージ済みになります。番号はイシューと同じで、変更は `refs/pull/番号/head` に有ります(`git fetch origin refs/pull/3/head:pr-3`)。リポジトリ毎に設定で無効に出来ます。 |
| 310 | |
| 311 | ### TLS 1.3・1.2(php_openssl は不要) |
| 312 | HTTPS のリクエスト(`Curl`、ActivityPub)、SMTP の STARTTLS(`Mailer`)、MySQL・PostgreSQL の TLS は PHP だけで書かれた TLS 1.3・1.2 のクライアント `Std\Lib\Tls`(`php_gmp` が必要)を使うので、`php_openssl` は不要です。RSA の鍵と署名(ActivityPub)は `Std\Lib\Crypto\Rsa` を使い、今までの PEM の鍵もそのまま使えます。 |
| 313 | * TLS 1.3 を優先し、TLS 1.2 にしか対応していないサーバーとは安全な暗号スイートだけで TLS 1.2 を使います(ECDHE と AES-GCM 又は ChaCha20-Poly1305、サーバーが対応していれば Extended Master Secret、再ネゴシエーションはしない)。CBC、RSA の鍵交換、DHE、TLS 1.1 以前は未対応です。TLS 1.3 に対応しているサーバーとの通信を攻撃者が TLS 1.2 に下げた場合は分かります。TLS 1.2 を拒否するには、オプションの `'min_version' => '1.3'` を使って下さい。 |
| 314 | * 証明書は `config/config.php` の `TLS_CAFILE`(信頼する CA の PEM ファイル)で確認します。空なら、普通のシステムの場所を探します(OpenBSD は `/etc/ssl/cert.pem`)。httpd・php-fpm の chroot の中では `/var/www/etc/ssl/cert.pem` になるので、システムのファイルをそこにコピーして下さい。Windows にはこのファイルが無いので、`TLS_CAFILE` を設定して下さい(例:curl の `cacert.pem`)。 |
| 315 | * 暗号スイート:TLS 1.3 は `TLS_CHACHA20_POLY1305_SHA256`、`TLS_AES_128_GCM_SHA256`、`TLS_AES_256_GCM_SHA384`、TLS 1.2 は `TLS_ECDHE_{ECDSA,RSA}_WITH_{CHACHA20_POLY1305_SHA256,AES_128_GCM_SHA256,AES_256_GCM_SHA384}`。鍵交換:x25519、secp256r1、secp384r1。署名:RSA-PSS、ECDSA(P-256、P-384、P-521)、RSA PKCS#1 v1.5(証明書と TLS 1.2 のみ)。セッションの再開、0-RTT、クライアント証明書、失効の確認は未対応です。 |
| 316 | * 暗号化は PHP で計算するので、大きいファイルのダウンロードは openssl より遅いです(数 MB/秒)。 |
| 317 | ```php |
| 318 | $s = \Std\Lib\Tls::connect('example.com', 443); |
| 319 | fwrite($s, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"); |
| 320 | echo stream_get_contents($s); |
| 321 | ``` |
| 322 | |
| 323 | ### コメント |
| 324 | `config/config.php` で `COMMENTS_ENABLED` を `true` にすると、全部のブログ記事の下にコメント欄が表示されます(書き込みには `AUTH_ENABLED` も必要)。 |
| 325 | * ログイン中のユーザーはコメント、返信、いいねが出来ます(Markdown:太字、斜体、取り消し線、コード、引用、リスト、リンク)。自分のコメントは削除出来、管理者とスタッフは全部のコメントを削除出来ます。JavaScript は不要です。 |
| 326 | * `BLOGS` で `'federate' => true` のブログは `FEDIINFO` のアカウントが投稿し(`apfederate`)、その投稿(と返信)への Fediverse からの公開・未収載の返信がコメントになります。Fediverse での編集と削除も反映します。 |
| 327 | * 新しいコメントは全部の管理者に、自分のコメントへの返信といいねは本人に `/notifications` で通知します。 |
| 328 | * コメントは `data/comments/`、通知は `data/notifications/` に保存します。 |
| 329 | |
| 330 | ### Cron |
| 331 | 定期的なタスク(例えば、新しいブログ投稿を ActivityPub のフォロワーに送る `apfederate`)はシステムの cron で実行します。\ |
| 332 | `config/config.php` で `CRON_ENABLED` を `true` にして(ActivityPub を使う場合は `FEDIINFO['domain']` も設定して)、 |
| 333 | php-fpm を実行するユーザーの crontab に以下の行を追加して下さい(`crontab -u www -e`): |
| 334 | ``` |
| 335 | * * * * * /usr/local/bin/php-8.5 /var/www/htdocs/cron.php |
| 336 | ``` |
| 337 | |
| 338 | ジョブはコマンドライン(又は管理画面)で管理します: |
| 339 | ```sh |
| 340 | php cron.php add "*/10 * * * *" apfederate |
| 341 | php cron.php list |
| 342 | php cron.php help |
| 343 | ``` |
| 344 | |
| 345 | タスクは `util.php` の `register_cron_tasks()` で登録します(`cron.php` と管理画面で使います)。組み込みのタスク: |
| 346 | * `apfederate`:新しいブログ投稿を ActivityPub のフォロワーに送る |
| 347 | * `apupdate`:プロフィール(アクター)の更新を ActivityPub のフォロワーに送る |
| 348 | * `authpurge`:7日以内にメールアドレスを確認しなかったアカウントを削除する(`AUTH_VERIFY_EMAIL` が有効の場合) |
| 349 | * `apdeliver`:マイクロブログのアクティビティを他のサーバーに配信する(マルチユーザーモード、毎分実行) |
| 350 | * `apcleanup`:90日より古いリモートの投稿とキャッシュを削除する(マルチユーザーモード、1日1回実行) |
| 351 | * `notify`:通知のメールを送り、SSH のプッシュで Gitler のリポジトリに溜めた通知と配信を処理する(`AUTH_ENABLED` が有効の場合、毎分実行) |
| 352 | * `gitlergc`:Gitler のリポジトリを整理する(`GITLER_ENABLED` が有効の場合、1日1回実行) |
| 353 | |
| 354 | ### マイクロブログ(マルチユーザーの Fediverse) |
| 355 | 既定では ActivityPub はシングルユーザーで、ブログのアクター(`/ap/actor`)が新しいブログ投稿をフォロワーに送ります。\ |
| 356 | `FEDIINFO['isSingleUser']` を `false` にすると(`AUTH_ENABLED` と `FEDIINFO['domain']` も必要)マルチユーザーモードになります: |
| 357 | * ブログのアクターは今まで通り動きます。 |
| 358 | * Fediverse のロールを持つユーザーはそれぞれアクター(`@ユーザー名@ドメイン`)になります。ロールは BAN されたユーザー以外の全員に付与され、管理者はユーザーのプロフィールの編集画面で外せます。 |
| 359 | * `/microblog` にはホーム・ローカル・連合のタイムライン、投稿(注意書き、公開範囲、画像・動画4つまで)、返信、いいね、ブースト、フォロー(承認制も可)、ブロック、通報、検索(`@user@host`、投稿の URL、`#タグ`、文字)があります。JavaScript は不要です。 |
| 360 | * 管理者は `/microblog/admin` でサーバーごとブロックしたり、通報を確認したり出来ます。 |
| 361 | |
| 362 | 配信は待ち行列に入るので、cron のジョブを追加して下さい: |
| 363 | ```sh |
| 364 | php cron.php add "* * * * *" apdeliver |
| 365 | php cron.php add "@daily" apcleanup |
| 366 | ``` |
| 367 | アップロード出来るのは画像(PNG・JPEG・GIF・WebP、20 MiB まで)と動画(MP4・MKV・WebM、400 MiB まで)で、メタデータ(Exif、XMP、テキスト、MP4 の撮影日時・位置等)を取り除きます。動画は変換しないので、ブラウザで再生出来る形式(H.264 等)でアップロードして下さい。\ |
| 368 | 大きいファイルを受け付ける様に、PHP の `upload_max_filesize` と `post_max_size`(添付4つ分)、httpd の `connection max request body`(既定は1 MiB、超えると「413 Payload Too Large」)も設定して下さい。動画を再生出来る様に、httpd の `types` に `include "/usr/share/misc/mime.types"` もお勧めします。 |
| 369 | 他のサーバーのアイコンや画像は `/microblog/media` 経由で表示するので、CSP の `img-src` は `'self'` のままで構いません(閲覧者の IP アドレスも相手のサーバーに渡りません)。画像は `data/ap/remote/media/` に1週間キャッシュします。\ |
| 370 | ユーザーの鍵(RSA、2048ビット)は初めて使う時に作ります。 |
| 371 | |
| 372 | ### 二段階認証 |
| 373 | `config/config.php` で `AUTH_2FA_ENABLED` を `true` にすると、各ユーザーがプロフィールの編集の「二段階認証」から有効に出来ます。`false` なら誰も使えず、有効にしていたユーザーもパスワードだけでログインします。\ |
| 374 | 設定画面には QR コードと秘密鍵(PC のパスワードマネージャー用)が表示されるので、認証アプリ(Aegis、Google Authenticator、KeePassXC 等)に登録して、表示された6桁のコードを入力すると有効になります。以後はパスワードの後にコードを入力します(TOTP、SHA-1、6桁、30秒。同じコードは2度使えません)。\ |
| 375 | 端末を失くした場合は、管理者がそのユーザーの設定画面(`/profile/ユーザー名/2fa`)で自分のパスワードを入力すると無効に出来ます。 |
| 376 | |
| 377 | ### 管理画面 |
| 378 | `config/config.php` で `ADMIN_ENABLED` を `true` にすると(`AUTH_ENABLED` も必要)、管理者のロールを持つユーザーのメニューに「管理」が出て、`/admin` を開けます: |
| 379 | * ユーザー:アカウントの一覧と検索、作成(登録が無効でも、メールアドレスの確認なし)、ロールの変更(全部外すと BAN)、メールアドレスを確認済みにする、全部のログインの取り消し、メールアドレス・パスワードの変更と二段階認証の無効化(この3つは自分のパスワードが必要)。表示名・アイコン等は今まで通りプロフィールの編集で変更します。 |
| 380 | * ログ:`log/` のファイル。最後の行、検索、ダウンロード、空にする、削除。 |
| 381 | * Cron:ジョブの追加、変更、有効・無効、削除、今すぐ実行と、システムの cron の前回の実行。 |
| 382 | * キャッシュ:`data/cache/` を種類毎に表示・削除(1件、種類毎、全部)と、Fediverse のキャッシュ(リモートのアクター、WebFinger、画像)の削除。 |
| 383 | * データベースの変換:上の変換をフォームで。読む元・書く先は JSON のデータ(`data/`)、SQLite のファイル、MySQL・MariaDB 又は PostgreSQL のサーバー、SQL のダンプ(アップロード又はサーバーのファイル、書く先はダウンロード又はサーバーに保存)、サイトのデータベース(`DBINFO`)。パスはサイトのディレクトリからの相対パスで、その外は使えません。アップロードしたファイルと変換したファイルは `data/tmp/convert/` に1時間残ります。大きなファイルをアップロードするには、`php.ini` の `upload_max_filesize` と `post_max_size` を大きくして下さい。 |
| 384 | |
| 385 | 新しいサイトには管理者が居なく、`data/` が空の場合も有ります。`ADMIN_SETUP_CODE` に長いランダムな文字列(例:`openssl rand -hex 16` の出力)を設定して `/admin` を開くと `/admin/setup` に移動するので、そのコードで最初の管理者を作ってログインします。その後 `ADMIN_SETUP_CODE` を `''` に戻して下さい。管理者が居ればこのページは使えません。 |
| 386 | |
| 387 | ## 必要な PHP モジュール |
| 388 | * php_fileinfo(OpenBSDでデフォルトで有効している) |
| 389 | * php_gmp |
| 390 | * php_mbstring(OpenBSDでデフォルトで有効している) |
| 391 | * php_zlib(Git、普通はPHPに組み込まれている) |