├── .github ├── renovate.json └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── authn_webhook.go ├── ayame.code-workspace ├── cmd └── ayame │ └── main.go ├── config.go ├── config_example.ini ├── connection.go ├── connection_log.go ├── disconnect_webhook.go ├── docs ├── DESIGN.md └── USE.md ├── errors.go ├── go.mod ├── go.sum ├── logger.go ├── messages.go ├── metrics.go ├── ok_handler.go ├── ok_handler_test.go ├── room.go ├── server.go ├── signaling_handler.go ├── signaling_handler_test.go ├── staticcheck.conf ├── webhook.go └── ws_messages.go /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "dependencyDashboard": false, 7 | "timezone": "Asia/Tokyo", 8 | "packageRules": [ 9 | { 10 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 11 | "platformAutomerge": true, 12 | "automerge": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches-ignore: 5 | - "master" 6 | tags-ignore: 7 | - "*" 8 | jobs: 9 | ci: 10 | name: ci 11 | runs-on: ubuntu-24.04 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: setup go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version-file: ./go.mod 18 | cache: true 19 | cache-dependency-path: ./go.sum 20 | - uses: dominikh/staticcheck-action@v1 21 | with: 22 | version: "2025.1.1" 23 | install-go: false 24 | - run: go version 25 | - run: go fmt . 26 | - run: go build -v . 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: "*" 5 | 6 | jobs: 7 | release: 8 | name: release 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-go@v5 13 | with: 14 | go-version-file: ./go.mod 15 | - name: Set version 16 | id: version 17 | run: | 18 | VERSION=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g") 19 | echo ::set-output name=version::$VERSION 20 | echo "Version $VERSION" 21 | - run: go install github.com/tcnksm/ghr@latest 22 | - run: | 23 | GOOS=linux GOARCH=amd64 go build -o dist/ayame_linux_amd64 cmd/ayame/main.go 24 | GOOS=linux GOARCH=arm64 go build -o dist/ayame_linux_arm64 cmd/ayame/main.go 25 | GOOS=darwin GOARCH=amd64 go build -o dist/ayame_darwin_amd64 cmd/ayame/main.go 26 | GOOS=darwin GOARCH=arm64 go build -o dist/ayame_darwin_arm64 cmd/ayame/main.go 27 | gzip dist/* 28 | - run: ghr -t "${{ secrets.GITHUB_TOKEN }}" -u "${{ github.repository_owner }}" -r "ayame" --replace "${{ steps.version.outputs.version }}" dist/ 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ayame binary 2 | bin/* 3 | config.ini 4 | 5 | # 新しいログ形式 6 | *.jsonl 7 | *.jsonl.* 8 | 9 | # 古いログ形式 10 | *.log 11 | *.log.* 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["ayame", "zerolog"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # リリースノート 2 | 3 | - UPDATE 4 | - 下位互換がある変更 5 | - ADD 6 | - 下位互換がある追加 7 | - CHANGE 8 | - 下位互換のない変更 9 | - FIX 10 | - バグ修正 11 | 12 | ## develop 13 | 14 | ## 2025.5.0 15 | 16 | - [ADD] ログのメッセージキー名とタイムスタンプキー名を指定できるようにする 17 | - `log_message_key_name` と `log_timestamp_key_name` を追加する 18 | - @voluntas 19 | 20 | ## 2025.4.0 21 | 22 | - [ADD] シグナリングで利用する WebSocket の HTTP ヘッダーをウェブフックにコピーする `copy_websocket_header_names` 設定を追加する 23 | - HTTP ヘッダー名は大文字小文字を区別しません 24 | - 複数のヘッダー名を指定できます 25 | - @voluntas 26 | 27 | ## 2025.3.0 28 | 29 | - [CHANGE] シグナリングログファイル名のデフォルトを `signaling.log` から `signaling.jsonl` に変更する 30 | - @voluntas 31 | - [CHANGE] ウェブフックログファイル名のデフォルトを `webhook.log` から `webhook.jsonl` に変更する 32 | - @voluntas 33 | - [ADD] ログ出力時にログのドメインを追加する 34 | - `"domain": "ayame"` と `"domain": "signaling"` と `"domain": "webhook"` をそれぞれのログに追加する 35 | - @voluntas 36 | - [ADD] シグナリングログで type を指定してフィルターできるようにする 37 | - `signaling_log_filters` に type を指定する 38 | - @voluntas 39 | - [ADD] ログローテーションの設定を追加する 40 | - `log_rotate_max_size` はデフォルト 200 MB 41 | - `log_rotate_max_backups` はデフォルト 7 日 42 | - `log_rotate_max_age` はデフォルト 30 日 43 | - `log_rotate_compress` はデフォルト `false` 44 | - @voluntas 45 | - [ADD] 標準出力にログを出力する `log_stdout` を追加する 46 | - デフォルト false 47 | - シグナリングログとウェブフックログも標準出力に出力する 48 | - `log_stdout` が `true` の場合、ファイル出力を行わない 49 | - @voluntas 50 | - [ADD] デバッグコンソールを出力する `debug_console_log` を追加する 51 | - デフォルト false 52 | - `debug` が `true` かつ `debug_console_log` が `true` の場合のみ有効 53 | - 標準出力にのみデバッグコンソールが出力されるようになり、ファイル出力はされない 54 | - @voluntas 55 | - [FIX] WebSocket 確立後にメッセージが送られてきたタイミングで `connectionId` を生成するように修正する 56 | - @voluntas 57 | 58 | ## 2025.2.0 59 | 60 | - [ADD] コンソールログを JSONL 形式で出力できるようにする 61 | - 設定ファイルに `console_log_json` を追加 62 | - デフォルト false 63 | - @voluntas 64 | - [ADD] コンソールログのカラー有効化を指定できるようにする 65 | - 設定ファイルに `console_log_color` を追加 66 | - デフォルト true 67 | - @voluntas 68 | 69 | ## 2025.1.0 70 | 71 | - [CHANGE] ayame.log を JSONL 形式で出力するようにする 72 | - @voluntas 73 | - [CHANGE] シグナリングで利用する WebSocket の設定を定数から設定できるように変更する 74 | - WebSocket の待ち受け時間を設定する `websocket_read_timeout_sec` を追加 75 | - WebSocket の Pong が送られてこないためタイムアウトにするまでの時間を設定する `websocket_pong_timeout_sec` を追加 76 | - WebSocket の Ping 送信の時間間隔を設定する `websocket_ping_interval_sec` を追加 77 | - @voluntas 78 | 79 | ### misc 80 | 81 | - [UPDATE] CI の dominikh/staticcheck-action を v1 固定する 82 | - @voluntas 83 | - [UPDATE] CI の staticcheck を 2025.1.1 に上げる 84 | - @voluntas 85 | - [UPDATE] go.mod の Go 1.24.1 に上げる 86 | - @voluntas 87 | 88 | ## 2024.1.0 89 | 90 | - [CHANGE] debug 設定が有効な場合にのみ、signaling.log に受信したシグナリングの生データを出力するように変更する 91 | - @Hexa 92 | - [ADD] シグナリングで任意のメッセージの送信を有効にする type_message 設定を追加する 93 | - デフォルト値: false(type: message 無効) 94 | - @Hexa 95 | - [ADD] シグナリングで任意のメッセージの送信時に指定する type: message を追加する 96 | - @Hexa 97 | 98 | ### misc 99 | 100 | - [UPDATE] CI の staticcheck を 2024.1.1 に上げる 101 | - @voluntas 102 | - [UPDATE] go.mod の Go 1.23.4 に上げる 103 | - @voluntas 104 | - [ADD] リリースバイナリに linux arm64 を追加する 105 | - @voluntas 106 | 107 | ## 2023.2.0 108 | 109 | - [CHANGE] websocket を公式に戻す 110 | - @voluntas 111 | - [CHANGE] lumberjack を公式に戻す 112 | - @voluntas 113 | 114 | ## 2023.1.0 115 | 116 | - [CHANGE] サンプル設定ファイルを ayame.example.ini から config_example.ini に変更する 117 | - @voluntas 118 | - [CHANGE] デフォルトの設定ファイル名を ayame.ini から config.ini へ変更する 119 | - @voluntas 120 | - [ADD] staticcheck を導入する 121 | - @voluntas 122 | - [ADD] standalone モードを追加する 123 | - standalone モード時に、type: connected を受信した場合は WebSocket を切断する 124 | - standalone モードでは WebSocket を切断するため、PING を送信しないようにする 125 | - @Hexa 126 | - [ADD] ヘルスチェック用の URL を追加する 127 | - @Hexa 128 | - [CHANGE] 設定ファイルを YAML から INI に変更する 129 | - @Hexa 130 | - [FIX] webhook log が出力されるように修正する 131 | - @Hexa 132 | - [UPADTE] handler に echo を使用するように変更する 133 | - @Hexa 134 | - [ADD] Prometheus に対応する 135 | - @Hexa 136 | 137 | ## 2022.2.0 138 | 139 | - [CHANGE] ログに github.com/rs/zerolog/log を利用するように変更する 140 | - @voluntas 141 | - [CHANGE] lumberjack を shiguredo/lumberjack/v3 に変更する 142 | - @voluntas 143 | - [CHANGE] websocket を shiguredo/websocket v1.6.0 に変更する 144 | - @voluntas 145 | 146 | ## 2022.1.3 147 | 148 | - [FIX] リリースバイナリが生成されないのを修正する 149 | - @voluntas 150 | 151 | ## 2022.1.2 152 | 153 | - [FIX] リリースバイナリが生成されないのを修正する 154 | - @voluntas 155 | 156 | ## 2022.1.1 157 | 158 | - [CHANGE] gox を利用しないリリース方式に変更する 159 | - @voluntas 160 | - [UPDATE] ビルドテストを Go 1.18 以上にする 161 | - @voluntas 162 | 163 | ## 2022.1.0 164 | 165 | - [UPDATE] actions/checkout@v3 に上げる 166 | - @voluntas 167 | - [UPDATE] GitHub Actions の Go を 1.18 に上げる 168 | - @voluntas 169 | - [UPDATE] toml を v1.0.0 に上げる 170 | - @voluntas 171 | - [UPDATE] rs/zerolog を v1.26.1 に上げる 172 | - @voluntas 173 | 174 | ## 2021.2.1 175 | 176 | - [UPDATE] GitHub Actions の Go を 1.17.1 に上げる 177 | - @voluntas 178 | - [UPDATE] rs/zerolog を v1.25.0 に上げる 179 | - @voluntas 180 | - [UPDATE] yaml.v3 に戻す 181 | - @voluntas 182 | 183 | ## 2021.2 184 | 185 | - [UPDATE] go.mod を Go 1.17 に上げる 186 | - @voluntas 187 | - [UPDATE] GitHub Actions の Go を 1.17 に上げる 188 | - @voluntas 189 | - [UPDATE] rs/zerolog を v1.21.0 に上げる 190 | - @voluntas 191 | - [UPDATE] rs/zerolog を v1.24.0 に上げる 192 | - @voluntas 193 | - [CHANGE] "github.com/goccy/go-yam" に変更する 194 | - @voluntas 195 | 196 | ## 2021.1 197 | 198 | - [ADD] GitHub Actions の Go を 1.16 に上げる 199 | - @voluntas 200 | - [UPDATE] go.mod を Go 1.16 に上げる 201 | - @voluntas 202 | 203 | ## 2020.1.5 204 | 205 | - [UPDATE] rs/zerolog を v1.20.0 に上げる 206 | - @voluntas 207 | - [UPDATE] yaml を v2.4.0 に上げる 208 | - @voluntas 209 | 210 | ## 2020.1.4 211 | 212 | - [ADD] 起動時に INFO で Ayame のバージョンをログに書く仕組みを追加 213 | - @voluntas 214 | - [ADD] GitHub Actions でリリースファイルをアップロードする仕組みを追加 215 | - 古い仕組みを整理 216 | - @voluntas 217 | - [UPDATE] go.mod を Go 1.15 に上げる 218 | - @voluntas 219 | 220 | ## 2020.1.3 221 | 222 | - [UPDATE] rs/zerolog を v1.19.0 に上げる 223 | - @voluntas 224 | - [UPDATE] gorilla/websocket を v1.4.2 に上げる 225 | - @voluntas 226 | - [UPDATE] yaml を v2.3.0 に上げる 227 | - @voluntas 228 | 229 | ## 2020.1.2 230 | 231 | **昔にリリースミスが発覚したため、master を 19.08.0 まで戻してから再度リリースを行った** 232 | 233 | ## 2020.1.1 234 | 235 | - [FIX] 受信したメッセージが null の場合に落ちるため、nil チェックを追加する 236 | - @kadoshita @Hexa 237 | 238 | ## 2020.1 239 | 240 | **全て 1 から書き直している** 241 | 242 | - [ADD] register メッセージで key と signalingKey のどちらかを指定できるようにする 243 | - signalingKey が優先される 244 | - 将来的に signalingKey のみになる 245 | - @voluntas 246 | - [ADD] accept メッセージで isExistUser 以外に isExistClient を送るようにする 247 | - 将来的に isExistClient のみになる 248 | - @voluntas 249 | - [ADD] 切断時にウェブフック通知を飛ばせるようにする 250 | - disconnect_webhook_url を設定 251 | - @voluntas @Hexa 252 | - [ADD] signaling.log を追加する 253 | - @voluntas @Hexa 254 | - [ADD] webhook.log を追加する 255 | - @voluntas @Hexa 256 | - [ADD] register メッセージで ayameClient / environment / libwebrtc の情報を追加する 257 | - 認証ウェブフック通知で含まれるようにする 258 | - @voluntas 259 | - [ADD] type: accept 時に connectionId を払い出すようにする 260 | - @voluntas 261 | - [CHANGE] コードベースを変更する 262 | - @voluntas @Hexa 263 | - [CHANGE] addr を listen_ipv4_address に変更する 264 | - @voluntas 265 | - [CHANGE] port を listen_port_number に変更する 266 | - @voluntas 267 | - [CHANGE] allow_origin 設定を削除する 268 | - @voluntas 269 | - [CHANGE] ロガーを zerolog に変更する 270 | - @voluntas 271 | - [CHANGE] ログローテーションを lumberjack に変更する 272 | - @voluntas 273 | - [CHANGE] サンプルを削除する 274 | - @voluntas 275 | - [CHANGE] 登録済みのあとに WebSocket 切断した場合、 type: bye を送信するようにする 276 | - @voluntas @Hexa 277 | - [CHANGE] ウェブフックの戻り値のステータスコード 200 以外はエラーにする 278 | - @voluntas @Hexa 279 | - [CHANGE] ウェブフックの JSON のキーを snake_case から camelCase にする 280 | - @voluntas 281 | - [CHANGE] clientId をオプション化する 282 | - @voluntas 283 | - [FIX] サーバ側の切断の WS の終了処理を適切に行う 284 | - @voluntas @Hexa 285 | - [FIX] ウェブソケットの最大メッセージを 1MB に制限する 286 | - @voluntas 287 | - [FIX] ayame.log にターミナル用のカラーコードを含めないようにする 288 | - @Hexa 289 | - [CHANGE] 指定したログレベルでの ayamne.log へのログ出力に対応する 290 | - @Hexa 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LDFLAGS = -ldflags '-w -s' 2 | 3 | .PHONY: ayame 4 | 5 | ayame: 6 | go build $(LDFLAGS) -o bin/$@ cmd/ayame/main.go 7 | 8 | .PHONY: darwin linux 9 | 10 | GOOS = $@ 11 | GOARCH = amd64 12 | 13 | linux darwin: 14 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build $(LDFLAGS) -o bin/ayame-$(GOOS) cmd/ayame/main.go 15 | 16 | check: 17 | go test ./... 18 | 19 | clean: 20 | rm -rf ayame 21 | 22 | .PHONY: lint 23 | 24 | lint: 25 | golangci-lint run ./... 26 | 27 | fmt: 28 | golangci-lint run ./... --fix 29 | 30 | .PHONY: init 31 | 32 | init: 33 | cp -n config_example.ini config.ini 34 | 35 | test: 36 | go test -race -v 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTC Signaling Server Ayame 2 | 3 | [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/OpenAyame/ayame.svg)](https://github.com/OpenAyame/ayame) 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | [![Actions Status](https://github.com/OpenAyame/ayame/workflows/Go%20Build%20&%20Format/badge.svg)](https://github.com/OpenAyame/ayame/actions) 6 | 7 | ## About Shiguredo's open source software 8 | 9 | We will not respond to PRs or issues that have not been discussed on Discord. Also, Discord is only available in Japanese. 10 | 11 | Please read before use. 12 | 13 | ## 時雨堂のオープンソースソフトウェアについて 14 | 15 | 利用前に をお読みください。 16 | 17 | ## WebRTC Signaling Server Ayame について 18 | 19 | WebRTC Signaling Server Ayame は WebRTC 向けのシグナリングサーバです。 20 | 21 | WebRTC の P2P でのみ動作します。また動作を 1 ルームを最大 2 名に制限することでコードを小さく保っています。 22 | 23 | ## OpenAyame プロジェクトについて 24 | 25 | OpenAyame は WebRTC Signaling Server Ayame をオープンソースとして公開し、 26 | 継続的に開発を行うことで WebRTC をより身近に、使いやすくするプロジェクトです。 27 | 28 | 詳細については下記をご確認ください。 29 | 30 | [OpenAyame プロジェクト](http://bit.ly/OpenAyame) 31 | 32 | ## 方針 33 | 34 | - シグナリングの仕様の破壊的変更を可能な限り行わない 35 | - Go のバージョンは定期的にアップデートを行う 36 | - 依存ライブラリは定期的にアップデートを行う 37 | 38 | ## 注意 39 | 40 | - Ayame は P2P にしか対応していません 41 | - Ayame は 1 ルーム最大 2 名までしか対応していません 42 | 43 | ## 使ってみる 44 | 45 | Ayame を使ってみたい人は [USE.md](docs/USE.md) をお読みください。 46 | 47 | ## Web SDK を使ってみる 48 | 49 | [Ayame Web SDK](https://github.com/OpenAyame/ayame-web-sdk) 50 | 51 | ## Web SDK サンプルを使ってみる 52 | 53 | [Ayame Web SDK サンプル](https://github.com/OpenAyame/ayame-web-sdk-samples) 54 | 55 | ## 仕組みの詳細を知りたい 56 | 57 | Ayame の仕組みを知りたい人は [OpenAyame/ayame\-spec](https://github.com/OpenAyame/ayame-spec) をお読みください。 58 | 59 | ## Ayame Labo を使ってみる 60 | 61 | Ayame 仕様と完全互換な STUN/TURN サーバやルーム認証を組み込んだ無料で利用可能なシグナリングサービスを時雨堂が提供しています。 62 | 63 | [Ayame Labo](https://ayame-labo.shiguredo.app/) 64 | 65 | ## ライセンス 66 | 67 | Apache License 2.0 68 | 69 | ```text 70 | Copyright 2019-2025, Shiguredo Inc. 71 | 72 | Licensed under the Apache License, Version 2.0 (the "License"); 73 | you may not use this file except in compliance with the License. 74 | You may obtain a copy of the License at 75 | 76 | http://www.apache.org/licenses/LICENSE-2.0 77 | 78 | Unless required by applicable law or agreed to in writing, software 79 | distributed under the License is distributed on an "AS IS" BASIS, 80 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 81 | See the License for the specific language governing permissions and 82 | limitations under the License. 83 | ``` 84 | 85 | ## Ayame 利用例 86 | 87 | - [tarakoKutibiru/UnityRenderStreaming\-Ayame\-Sample](https://github.com/tarakoKutibiru/UnityRenderStreaming-Ayame-Sample) 88 | - [tarukosu/MixedReality\-WebRTC\-ayame: MixedReality\-WebRTC にて、シグナリングサーバとして Ayame を利用するためのコード](https://github.com/tarukosu/MixedReality-WebRTC-ayame) 89 | - [MixedReality\-WebRTC と Ayame Labo を利用して Unity で WebRTC を使う](https://zenn.dev/tarukosu/articles/20210220-webrtc-ayame) 90 | - [kadoshita/kisei\-online: 手軽に使える,オンライン帰省用ビデオ通話ツール](https://github.com/kadoshita/kisei-online) 91 | - [hakobera/serverless\-webrtc\-signaling\-server: Serverless WebRTC Signaling Server only works for WebRTC P2P\.](https://github.com/hakobera/serverless-webrtc-signaling-server) 92 | - [mganeko/react\_ts\_ayame: React\.js and Typescript example for Ayame Labo \(WebRTC signaling\)](https://github.com/mganeko/react_ts_ayame) 93 | - [mganeko/react\_ts\_ayame\_recv: React\.js and Typescript example for Ayame Labo \(WebRTC signaling\)](https://github.com/mganeko/react_ts_ayame_recv) 94 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2025.5.0 2 | -------------------------------------------------------------------------------- /authn_webhook.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/url" 8 | "time" 9 | ) 10 | 11 | type authnWebhookRequest struct { 12 | RoomID string `json:"roomId"` 13 | ClientID string `json:"clientId"` 14 | ConnectionID string `json:"connectionId"` 15 | SignalingKey *string `json:"signalingKey,omitempty"` 16 | AuthnMetadata *interface{} `json:"authnMetadata,omitempty"` 17 | AyameClient *string `json:"ayameClient,omitempty"` 18 | Libwebrtc *string `json:"libwebrtc,omitempty"` 19 | Environment *string `json:"environment,omitempty"` 20 | } 21 | 22 | type authnWebhookResponse struct { 23 | Allowed *bool `json:"allowed"` 24 | IceServers *[]iceServer `json:"iceServers"` 25 | Reason *string `json:"reason"` 26 | AuthzMetadata *interface{} `json:"authzMetadata"` 27 | } 28 | 29 | func (c *connection) authnWebhook() (*authnWebhookResponse, error) { 30 | if c.config.AuthnWebhookURL == "" { 31 | var allowed = true 32 | authnWebhookResponse := &authnWebhookResponse{Allowed: &allowed} 33 | return authnWebhookResponse, nil 34 | } 35 | 36 | req := &authnWebhookRequest{ 37 | RoomID: c.roomID, 38 | ClientID: c.clientID, 39 | ConnectionID: c.ID, 40 | SignalingKey: c.signalingKey, 41 | AuthnMetadata: c.authnMetadata, 42 | AyameClient: c.ayameClient, 43 | Libwebrtc: c.libwebrtc, 44 | Environment: c.environment, 45 | } 46 | 47 | start := time.Now() 48 | 49 | resp, err := c.postRequest(c.config.AuthnWebhookURL, req) 50 | if err != nil { 51 | c.errLog().Err(err).Caller().Msg("AuthnWebhookError") 52 | return nil, errAuthnWebhook 53 | } 54 | // http://ikawaha.hateblo.jp/entry/2015/06/07/074155 55 | defer resp.Body.Close() 56 | 57 | c.webhookLog("authnReq", req) 58 | 59 | u, err := url.Parse(c.config.AuthnWebhookURL) 60 | if err != nil { 61 | c.errLog().Err(err).Caller().Msg("AuthnWebhookError") 62 | return nil, errAuthnWebhook 63 | } 64 | statusCode := fmt.Sprintf("%d", resp.StatusCode) 65 | m := c.metrics 66 | m.IncWebhookReqCnt(statusCode, "POST", u.Host, u.Path) 67 | m.ObserveWebhookReqDur(statusCode, "POST", u.Host, u.Path, time.Since(start).Seconds()) 68 | // TODO: ヘッダーのサイズも計測する 69 | m.ObserveWebhookReqSz(statusCode, "POST", u.Host, u.Path, resp.Request.ContentLength) 70 | m.ObserveWebhookResSz(statusCode, "POST", u.Host, u.Path, resp.ContentLength) 71 | 72 | body, err := io.ReadAll(resp.Body) 73 | if err != nil { 74 | c.errLog().Bytes("body", body).Err(err).Caller().Msg("AuthnWebhookResponseError") 75 | return nil, err 76 | } 77 | 78 | // ログ出力用 79 | httpResponse := &httpResponse{ 80 | Status: resp.Status, 81 | Proto: resp.Proto, 82 | Header: resp.Header, 83 | Body: string(body), 84 | } 85 | 86 | // 200 以外で返ってきたときはエラーとする 87 | if resp.StatusCode != 200 { 88 | c.errLog().Interface("resp", httpResponse).Caller().Msg("AuthnWebhookUnexpectedStatusCode") 89 | return nil, errAuthnWebhookUnexpectedStatusCode 90 | } 91 | 92 | c.webhookLog("authnResp", httpResponse) 93 | 94 | authnWebhookResponse := authnWebhookResponse{} 95 | if err := json.Unmarshal(body, &authnWebhookResponse); err != nil { 96 | c.errLog().Err(err).Caller().Msg("AuthnWebhookResponseError") 97 | return nil, errAuthnWebhookResponse 98 | } 99 | 100 | if authnWebhookResponse.Reason == nil { 101 | m.IncAuthnWebhookCnt(statusCode, "POST", u.Host, u.Path, *authnWebhookResponse.Allowed, "") 102 | } else { 103 | m.IncAuthnWebhookCnt(statusCode, "POST", u.Host, u.Path, *authnWebhookResponse.Allowed, *authnWebhookResponse.Reason) 104 | } 105 | 106 | return &authnWebhookResponse, nil 107 | } 108 | -------------------------------------------------------------------------------- /ayame.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /cmd/ayame/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "strconv" 10 | 11 | "github.com/OpenAyame/ayame" 12 | zlog "github.com/rs/zerolog/log" 13 | "golang.org/x/sync/errgroup" 14 | ) 15 | 16 | func main() { 17 | // bin/ayame -V 18 | showVersion := flag.Bool("V", false, "バージョン") 19 | 20 | // bin/ayame -C config.ini 21 | configFilePath := flag.String("C", "./config.ini", "設定ファイルへのパス") 22 | flag.Parse() 23 | 24 | if *showVersion { 25 | fmt.Printf("WebRTC Signaling Server Ayame version %s\n", ayame.Version) 26 | return 27 | } 28 | 29 | config, err := ayame.NewConfig(*configFilePath) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | // グローバルの logger を初期化 35 | err = ayame.InitLogger(config) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | // ayame.jsonl ロガーを用意 41 | logger, err := ayame.NewLogger(config, config.LogName, "ayame") 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | // ayame.jsonl は グローバルの logger に代入する 46 | zlog.Logger = *logger 47 | 48 | config.PrintConfig() 49 | 50 | server, err := ayame.NewServer(config) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | ctx, cancel := context.WithCancel(context.Background()) 56 | defer cancel() 57 | 58 | g, ctx := errgroup.WithContext(ctx) 59 | 60 | g.Go(func() error { 61 | addressAndPort := net.JoinHostPort(config.ListenPrometheusIPv4Address, strconv.Itoa(int(config.ListenPrometheusPortNumber))) 62 | return server.EchoPrometheus.Start(addressAndPort) 63 | }) 64 | 65 | g.Go(func() error { 66 | return server.Start(ctx) 67 | }) 68 | 69 | g.Go(func() error { 70 | return server.StartMatchServer(ctx) 71 | }) 72 | 73 | if err := g.Wait(); err != nil { 74 | log.Fatal(err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | _ "embed" 5 | "net/url" 6 | 7 | zlog "github.com/rs/zerolog/log" 8 | "gopkg.in/ini.v1" 9 | ) 10 | 11 | //go:embed VERSION 12 | var Version string 13 | 14 | const ( 15 | defaultLogDir = "." 16 | defaultLogName = "ayame.jsonl" 17 | 18 | // 200 MB 19 | defaultLogRotateMaxSize = 200 20 | // 7 ファイル 21 | defaultLogRotateMaxBackups = 7 22 | // 30 日 23 | defaultLogRotateMaxAge = 30 24 | 25 | defaultSignalingLogName = "signaling.jsonl" 26 | 27 | defaultListenIPv4Address = "0.0.0.0" 28 | defaultListenPortNumber = 3000 29 | 30 | defaultWebSocketReadTimeoutSec = 90 31 | defaultWebSocketPongTimeoutSec = 60 32 | defaultWebSocketPingIntervalSec = 5 33 | 34 | defaultWebhookRequestTimeout = 5 35 | defaultWebhookLogName = "webhook.jsonl" 36 | 37 | defaultListenPrometheusIPv4Address = "0.0.0.0" 38 | defaultListenPrometheusPortNumber = 4000 39 | ) 40 | 41 | var defaultSignalingLogFilters = []string{ 42 | "register", 43 | "offer", 44 | "answer", 45 | "candidate", 46 | "connected", 47 | "message", 48 | } 49 | 50 | type Config struct { 51 | Debug bool `ini:"debug"` 52 | 53 | LogDir string `ini:"log_dir"` 54 | LogName string `ini:"log_name"` 55 | LogStdout bool `ini:"log_stdout"` 56 | LogRotateMaxSize int `ini:"log_rotate_max_size"` 57 | LogRotateMaxBackups int `ini:"log_rotate_max_backups"` 58 | LogRotateMaxAge int `ini:"log_rotate_max_age"` 59 | LogRotateCompress bool `ini:"log_rotate_compress"` 60 | 61 | LogMessageKeyName string `ini:"log_message_key_name"` 62 | LogTimestampKeyName string `ini:"log_timestamp_key_name"` 63 | 64 | SignalingLogName string `ini:"signaling_log_name"` 65 | SignalingLogFilters []string `ini:"signaling_log_filters"` 66 | 67 | DebugConsoleLog bool `ini:"debug_console_log"` 68 | DebugConsoleLogJSON bool `ini:"debug_console_log_json"` 69 | 70 | TypeMessage bool `ini:"type_message"` 71 | 72 | ListenIPv4Address string `ini:"listen_ipv4_address"` 73 | ListenPortNumber int32 `ini:"listen_port_number"` 74 | 75 | // socket の待ち受け時間 76 | WebSocketReadTimeoutSec int32 `ini:"websocket_read_timeout_sec"` 77 | // pong が送られてこないためタイムアウトにするまでの時間 78 | WebSocketPongTimeoutSec int32 `ini:"websocket_pong_timeout_sec"` 79 | // ping 送信の時間間隔 80 | WebSocketPingIntervalSec int32 `ini:"websocket_ping_interval_sec"` 81 | 82 | // シグナリングからコピーする WebSocket の HTTP ヘッダー名 83 | CopyWebSocketHeaderNames []string `ini:"copy_websocket_header_names"` 84 | 85 | AuthnWebhookURL string `ini:"authn_webhook_url"` 86 | DisconnectWebhookURL string `ini:"disconnect_webhook_url"` 87 | 88 | WebhookLogName string `ini:"webhook_log_name"` 89 | WebhookRequestTimeoutSec int32 `ini:"webhook_request_timeout_sec"` 90 | 91 | ListenPrometheusIPv4Address string `ini:"listen_prometheus_ipv4_address"` 92 | ListenPrometheusPortNumber int32 `ini:"listen_prometheus_port_number"` 93 | } 94 | 95 | func NewConfig(configFilePath string) (*Config, error) { 96 | config := new(Config) 97 | 98 | iniConfig, err := ini.InsensitiveLoad(configFilePath) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | if err := iniConfig.StrictMapTo(config); err != nil { 104 | return nil, err 105 | } 106 | 107 | if config.AuthnWebhookURL != "" { 108 | if _, err := url.ParseRequestURI(config.AuthnWebhookURL); err != nil { 109 | return nil, err 110 | } 111 | } 112 | 113 | if config.DisconnectWebhookURL != "" { 114 | if _, err := url.ParseRequestURI(config.DisconnectWebhookURL); err != nil { 115 | return nil, err 116 | } 117 | } 118 | 119 | setDefaultsConfig(config) 120 | 121 | return config, nil 122 | } 123 | 124 | func setDefaultsConfig(config *Config) { 125 | if config.LogDir == "" { 126 | config.LogDir = defaultLogDir 127 | } 128 | 129 | if config.LogName == "" { 130 | config.LogName = defaultLogName 131 | } 132 | 133 | if config.LogRotateMaxSize == 0 { 134 | config.LogRotateMaxSize = defaultLogRotateMaxSize 135 | } 136 | 137 | if config.LogRotateMaxBackups == 0 { 138 | config.LogRotateMaxBackups = defaultLogRotateMaxBackups 139 | } 140 | 141 | if config.LogRotateMaxAge == 0 { 142 | config.LogRotateMaxAge = defaultLogRotateMaxAge 143 | } 144 | 145 | if config.SignalingLogName == "" { 146 | config.SignalingLogName = defaultSignalingLogName 147 | } 148 | 149 | if config.SignalingLogFilters == nil { 150 | config.SignalingLogFilters = defaultSignalingLogFilters 151 | } 152 | 153 | if config.ListenIPv4Address == "" { 154 | config.ListenIPv4Address = defaultListenIPv4Address 155 | } 156 | 157 | if config.ListenPortNumber == 0 { 158 | config.ListenPortNumber = defaultListenPortNumber 159 | } 160 | 161 | if config.WebSocketReadTimeoutSec == 0 { 162 | config.WebSocketReadTimeoutSec = defaultWebSocketReadTimeoutSec 163 | } 164 | 165 | if config.WebSocketPongTimeoutSec == 0 { 166 | config.WebSocketPongTimeoutSec = defaultWebSocketPongTimeoutSec 167 | } 168 | 169 | if config.WebSocketPingIntervalSec == 0 { 170 | config.WebSocketPingIntervalSec = defaultWebSocketPingIntervalSec 171 | } 172 | 173 | if config.WebhookRequestTimeoutSec == 0 { 174 | config.WebhookRequestTimeoutSec = defaultWebhookRequestTimeout 175 | } 176 | 177 | if config.WebhookLogName == "" { 178 | config.WebhookLogName = defaultWebhookLogName 179 | } 180 | 181 | if config.ListenPrometheusIPv4Address == "" { 182 | config.ListenPrometheusIPv4Address = defaultListenPrometheusIPv4Address 183 | } 184 | 185 | if config.ListenPrometheusPortNumber == 0 { 186 | config.ListenPrometheusPortNumber = defaultListenPrometheusPortNumber 187 | } 188 | 189 | } 190 | 191 | func (c *Config) PrintConfig() { 192 | zlog.Info().Bool("debug", c.Debug).Msg("AyameConf") 193 | 194 | zlog.Info().Str("log_dir", c.LogDir).Msg("AyameConf") 195 | zlog.Info().Str("log_name", c.LogName).Msg("AyameConf") 196 | zlog.Info().Bool("log_stdout", c.LogStdout).Msg("AyameConf") 197 | 198 | zlog.Info().Int("log_rotate_max_size", c.LogRotateMaxSize).Msg("AyameConf") 199 | zlog.Info().Int("log_rotate_max_backups", c.LogRotateMaxBackups).Msg("AyameConf") 200 | zlog.Info().Int("log_rotate_max_age", c.LogRotateMaxAge).Msg("AyameConf") 201 | zlog.Info().Bool("log_rotate_compress", c.LogRotateCompress).Msg("AyameConf") 202 | 203 | zlog.Info().Str("signaling_log_name", c.SignalingLogName).Msg("AyameConf") 204 | zlog.Info().Strs("signaling_log_filters", c.SignalingLogFilters).Msg("AyameConf") 205 | 206 | zlog.Info().Bool("debug_console_log", c.DebugConsoleLog).Msg("AyameConf") 207 | zlog.Info().Bool("debug_console_log_json", c.DebugConsoleLogJSON).Msg("AyameConf") 208 | 209 | zlog.Info().Str("listen_ipv4_address", c.ListenIPv4Address).Msg("AyameConf") 210 | zlog.Info().Int32("listen_port_number", c.ListenPortNumber).Msg("AyameConf") 211 | 212 | zlog.Info().Strs("copy_websocket_header_names", c.CopyWebSocketHeaderNames).Msg("AyameConf") 213 | 214 | zlog.Info().Str("authn_webhook_url", c.AuthnWebhookURL).Msg("AyameConf") 215 | zlog.Info().Str("disconnect_webhook_url", c.DisconnectWebhookURL).Msg("AyameConf") 216 | 217 | zlog.Info().Str("webhook_log_name", c.WebhookLogName).Msg("AyameConf") 218 | zlog.Info().Int32("webhook_request_timeout_sec", c.WebhookRequestTimeoutSec).Msg("AyameConf") 219 | 220 | zlog.Info().Str("prometheus_ipv4_address", c.ListenPrometheusIPv4Address).Msg("AyameConf") 221 | zlog.Info().Int32("prometheus_port", c.ListenPrometheusPortNumber).Msg("AyameConf") 222 | } 223 | -------------------------------------------------------------------------------- /config_example.ini: -------------------------------------------------------------------------------- 1 | ## ログの出力先ディレクトリを指定します 2 | ## デフォルトは . です 3 | # log_dir = . 4 | 5 | ## ログのファイル名を指定します 6 | ## デフォルトは ayame.jsonl です 7 | # log_name = ayame.jsonl 8 | 9 | ## 標準出力にログを出力するかを指定します 10 | ## デフォルトは false です 11 | # log_stdout = true 12 | 13 | ## ローテーションするログの最大サイズ(MB)を指定します 14 | ## デフォルトは 200 です 15 | # log_rotate_max_size = 200 16 | 17 | ## 保持するログファイルの最大数を指定します 18 | ## デフォルトは 7 です 19 | # log_rotate_max_backups = 7 20 | 21 | ## 古いログファイルを保持する最大日数を指定します 22 | ## デフォルトは 30 です 23 | # log_rotate_max_age = 30 24 | 25 | ## ローテーションするログの圧縮 (gzip)を有効にするかを指定します 26 | ## デフォルトは false です 27 | # log_rotate_compress = true 28 | 29 | ## ログのメッセージキー名を指定します 30 | ## デフォルトは message です 31 | # log_message_key_name = message 32 | 33 | ## ログのタイムスタンプキー名を指定します 34 | ## デフォルトは time です 35 | # log_timestamp_key_name = time 36 | 37 | ## シグナリングログのファイル名を指定します 38 | ## デフォルトは signaling.jsonl です 39 | # signaling_log_name = signaling.jsonl 40 | 41 | ## シグナリングログで出力する type を指定します 42 | ## デフォルトは全て出力します 43 | ## register,offer,answer,candidate,connected,message が指定できます 44 | # signaling_log_filters = offer,answer 45 | 46 | ## デバッグモードを有効にするかを指定します 47 | ## デフォルトは false です 48 | # debug = true 49 | 50 | ## デバッグコンソールログを出力するかを指定します 51 | ## デフォルトは false です 52 | # debug_console_log = true 53 | 54 | ## デバッグコンソールログを JSON 形式で出力するかを指定します 55 | ## デフォルトは false です 56 | # debug_console_log_json = true 57 | 58 | ## type: "message" を送受信できるようにするかを指定します 59 | ## デフォルトは false です 60 | # type_message = true 61 | 62 | ## 待ち受ける IPv4 アドレスを指定します 63 | ## デフォルトは 0.0.0.0 です 64 | # listen_ipv4_address = 0.0.0.0 65 | 66 | ## 待ち受けるポート番号を指定します 67 | ## デフォルトは 3000 です 68 | # listen_port_number = 3000 69 | 70 | ## WebSocket の読み込みタイムアウト時間を指定します 71 | ## デフォルトは 90 秒です 72 | # websocket_read_timeout_sec = 90 73 | 74 | ## WebSocket の Pong タイムアウト時間を指定します 75 | ## デフォルトは 60 秒です 76 | # websocket_pong_timeout_sec = 60 77 | 78 | ## WebSocket の Ping 送信間隔を指定します 79 | ## デフォルトは 5 秒です 80 | # websocket_ping_interval_sec = 5 81 | 82 | ## シグナリングで利用する WebSocket の HTTP ヘッダーをウェブフックにコピーするヘッダー名を指定します 83 | ## デフォルトは未指定です 84 | # copy_websocket_header_names = X-Forwarded-For, X-Real-IP 85 | 86 | ## 認証 Webhook の URL を指定します 87 | ## デフォルトは未指定です 88 | # authn_webhook_url = http://127.0.0.1:3001/ayame/webhook/authn 89 | 90 | ## 切断 Webhook の URL を指定します 91 | ## デフォルトは未指定です 92 | # disconnect_webhook_url = http://127.0.0.1:3001/ayame/webhook/disconnect 93 | 94 | ## Webhook のリクエストタイムアウト時間を指定します 95 | ## デフォルトは 5 秒です 96 | # webhook_request_timeout_sec = 5 97 | 98 | ## Webhook のログファイル名を指定します 99 | ## デフォルトは webhook.jsonl です 100 | # webhook_log_name = webhook.jsonl 101 | 102 | ## 待ち受ける Prometheus の IPv4 アドレスを指定します 103 | ## デフォルトは 0.0.0.0 です 104 | # listen_prometheus_ipv4_address = 0.0.0.0 105 | 106 | ## 待ち受ける Prometheus のポート番号を指定します 107 | ## デフォルトは 4000 です 108 | # listen_prometheus_port_number = 4000 109 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "math/rand" 8 | "time" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/oklog/ulid/v2" 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | type connection struct { 16 | ID string 17 | roomID string 18 | clientID string 19 | authnMetadata *interface{} 20 | signalingKey *string 21 | 22 | // クライアント情報 23 | ayameClient *string 24 | environment *string 25 | libwebrtc *string 26 | 27 | authzMetadata *interface{} 28 | 29 | // WebSocket コネクション 30 | wsConn *websocket.Conn 31 | 32 | // レジスターされているかどうか 33 | registered bool 34 | 35 | // 転送用のチャネル 36 | forwardChannel chan forward 37 | 38 | // config 39 | config Config 40 | 41 | signalingLogger zerolog.Logger 42 | webhookLogger zerolog.Logger 43 | 44 | // standalone mode 45 | standalone bool 46 | 47 | // シグナリングからコピーした HTTP ヘッダー 48 | copyHeaders map[string]string 49 | 50 | metrics *Metrics 51 | } 52 | 53 | func (c *connection) SendJSON(v interface{}) error { 54 | if err := c.wsConn.WriteJSON(v); err != nil { 55 | c.errLog().Err(err).Interface("msg", v).Msg("FailedToSendMsg") 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | func (c *connection) sendPingMessage() error { 62 | msg := &pingMessage{ 63 | Type: "ping", 64 | } 65 | 66 | if err := c.SendJSON(msg); err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | // reason の長さが不十分そうな場合は CloseMessage ではなく TextMessage を使用するように変更する 74 | func (c *connection) sendCloseMessage(code int, reason string) error { 75 | deadline := time.Now().Add(writeWait) 76 | closeMessage := websocket.FormatCloseMessage(code, reason) 77 | return c.wsConn.WriteControl(websocket.CloseMessage, closeMessage, deadline) 78 | } 79 | 80 | func (c *connection) sendAcceptMessage(isExistClient bool, iceServers *[]iceServer, authzMetadata *interface{}) error { 81 | msg := &acceptMessage{ 82 | Type: "accept", 83 | ConnectionID: c.ID, 84 | IsExistClient: isExistClient, 85 | // 下位互換性 86 | IsExistUser: isExistClient, 87 | AuthzMetadata: authzMetadata, 88 | IceServers: iceServers, 89 | } 90 | 91 | if err := c.SendJSON(msg); err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | 97 | func (c *connection) sendRejectMessage(reason string) error { 98 | msg := &rejectMessage{ 99 | Type: "reject", 100 | Reason: reason, 101 | } 102 | 103 | if err := c.SendJSON(msg); err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | func (c *connection) sendByeMessage() error { 110 | msg := &byeMessage{ 111 | Type: "bye", 112 | } 113 | 114 | if err := c.SendJSON(msg); err != nil { 115 | return err 116 | } 117 | return nil 118 | } 119 | 120 | func (c *connection) closeWs() { 121 | c.wsConn.Close() 122 | c.debugLog().Msg("CLOSED-WS") 123 | } 124 | 125 | func (c *connection) register() int { 126 | resultChannel := make(chan int) 127 | registerChannel <- ®ister{ 128 | connection: c, 129 | resultChannel: resultChannel, 130 | } 131 | // ここはブロックする candidate とかを並列で来てるかもしれないが知らん 132 | result := <-resultChannel 133 | // もう server で触ることはないのでここで閉じる 134 | close(resultChannel) 135 | return result 136 | } 137 | 138 | func (c *connection) unregister() { 139 | if c.registered { 140 | unregisterChannel <- &unregister{ 141 | connection: c, 142 | } 143 | } 144 | } 145 | 146 | func (c *connection) forward(msg []byte) { 147 | // グローバルにあるチャンネルに対して投げ込む 148 | forwardChannel <- forward{ 149 | connection: c, 150 | rawMessage: msg, 151 | } 152 | } 153 | 154 | func (c *connection) main(cancel context.CancelFunc, messageChannel chan []byte) { 155 | pongTimeoutTimer := time.NewTimer(time.Duration(c.config.WebSocketPongTimeoutSec) * time.Second) 156 | pingTimer := time.NewTimer(time.Duration(c.config.WebSocketPingIntervalSec) * time.Second) 157 | 158 | defer func() { 159 | timerStop(pongTimeoutTimer) 160 | timerStop(pingTimer) 161 | // キャンセルを呼ぶ 162 | cancel() 163 | c.debugLog().Msg("CANCEL") 164 | // アンレジはここでやる 165 | c.unregister() 166 | c.debugLog().Msg("UNREGISTER") 167 | c.debugLog().Msg("EXIT-MAIN") 168 | }() 169 | 170 | loop: 171 | for { 172 | select { 173 | case <-pingTimer.C: 174 | if !c.standalone { 175 | if err := c.sendPingMessage(); err != nil { 176 | break loop 177 | } 178 | } 179 | pingTimer.Reset(time.Duration(c.config.WebSocketPingIntervalSec) * time.Second) 180 | case <-pongTimeoutTimer.C: 181 | if !c.standalone { 182 | // タイマーが発火してしまったので切断する 183 | c.errLog().Msg("PongTimeout") 184 | break loop 185 | } 186 | case rawMessage, ok := <-messageChannel: 187 | // message チャンネルが閉じられた、main 終了待ち 188 | if !ok { 189 | c.debugLog().Msg("CLOSED-MESSAGE-CHANNEL") 190 | // メッセージチャネルが閉じてるので return でもう抜けてしまう 191 | return 192 | } 193 | if err := c.handleWsMessage(rawMessage, pongTimeoutTimer); err != nil { 194 | // ここのエラーのログはすでに handleWsMessage でとってあるので不要 195 | // エラーになったら抜ける 196 | break loop 197 | } 198 | case forward, ok := <-c.forwardChannel: 199 | if !ok { 200 | // server 側で forwardChannel を閉じた 201 | c.debugLog().Msg("UNREGISTERED") 202 | if !c.standalone { 203 | if err := c.sendByeMessage(); err != nil { 204 | c.errLog().Err(err).Msg("FailedSendByeMessage") 205 | // 送れなかったら閉じるメッセージも送れないので return 206 | return 207 | } 208 | c.debugLog().Msg("SENT-BYE-MESSAGE") 209 | } 210 | break loop 211 | } 212 | if err := c.wsConn.WriteMessage(websocket.TextMessage, forward.rawMessage); err != nil { 213 | c.errLog().Err(err).Msg("FailedWriteMessage") 214 | // 送れなかったら閉じるメッセージも送れないので return 215 | return 216 | } 217 | } 218 | } 219 | 220 | // こちらの都合で終了するので Websocket 終了のお知らせを送る 221 | if err := c.sendCloseMessage(websocket.CloseNormalClosure, ""); err != nil { 222 | c.debugLog().Err(err).Msg("FAILED-SEND-CLOSE-MESSAGE") 223 | // 送れなかったら return する 224 | return 225 | } 226 | c.debugLog().Msg("SENT-CLOSE-MESSAGE") 227 | } 228 | 229 | func (c *connection) wsRecv(ctx context.Context, messageChannel chan []byte) { 230 | loop: 231 | for { 232 | readDeadline := time.Now().Add(time.Duration(c.config.WebSocketReadTimeoutSec) * time.Second) 233 | if err := c.wsConn.SetReadDeadline(readDeadline); err != nil { 234 | c.errLog().Err(err).Msg("FailedSetReadDeadLine") 235 | break loop 236 | } 237 | _, rawMessage, err := c.wsConn.ReadMessage() 238 | if err != nil { 239 | // ここに来るのはほぼ WebSocket が切断されたとき 240 | c.debugLog().Err(err).Msg("WS-READ-MESSAGE-ERROR") 241 | break loop 242 | } 243 | messageChannel <- rawMessage 244 | } 245 | close(messageChannel) 246 | c.debugLog().Msg("CLOSE-MESSAGE-CHANNEL") 247 | // メインが死ぬまで待つ 248 | <-ctx.Done() 249 | c.debugLog().Msg("EXITED-MAIN") 250 | if !c.standalone { 251 | c.closeWs() 252 | } 253 | c.debugLog().Msg("EXIT-WS-RECV") 254 | 255 | if err := c.disconnectWebhook(); err != nil { 256 | c.errLog().Err(err).Caller().Msg("DisconnectWebhookError") 257 | return 258 | } 259 | } 260 | 261 | // メッセージ系のエラーログはここですべて取る 262 | func (c *connection) handleWsMessage(rawMessage []byte, pongTimeoutTimer *time.Timer) error { 263 | message := &message{} 264 | if err := json.Unmarshal(rawMessage, &message); err != nil { 265 | c.errLog().Err(err).Bytes("rawMessage", rawMessage).Msg("InvalidJSON") 266 | return errInvalidJSON 267 | } 268 | 269 | if message == nil { 270 | c.errLog().Bytes("rawMessage", rawMessage).Msg("UnexpectedJSON") 271 | return errUnexpectedJSON 272 | } 273 | 274 | switch message.Type { 275 | case "pong": 276 | timerStop(pongTimeoutTimer) 277 | pongTimeoutTimer.Reset(time.Duration(c.config.WebSocketPongTimeoutSec) * time.Second) 278 | case "register": 279 | // すでに登録されているのにもう一度登録しに来た 280 | if c.registered { 281 | c.errLog().Bytes("rawMessage", rawMessage).Msg("InternalServer") 282 | return errInternalServer 283 | } 284 | 285 | registerMessage := ®isterMessage{} 286 | if err := json.Unmarshal(rawMessage, ®isterMessage); err != nil { 287 | c.errLog().Err(err).Bytes("rawMessage", rawMessage).Msg("InvalidRegisterMessageJSON") 288 | return errInvalidJSON 289 | } 290 | 291 | if registerMessage.RoomID == "" { 292 | c.errLog().Bytes("rawMessage", rawMessage).Msg("MissingRoomID") 293 | return errMissingRoomID 294 | } 295 | c.roomID = registerMessage.RoomID 296 | 297 | c.clientID = registerMessage.ClientID 298 | if registerMessage.ClientID == "" { 299 | c.clientID = c.ID 300 | } 301 | 302 | // 下位互換性 303 | if registerMessage.Key != nil { 304 | c.signalingKey = registerMessage.Key 305 | } 306 | 307 | if registerMessage.SignalingKey != nil { 308 | c.signalingKey = registerMessage.SignalingKey 309 | } 310 | 311 | c.authnMetadata = registerMessage.AuthnMetadata 312 | c.standalone = registerMessage.Standalone 313 | 314 | // クライアント情報の登録 315 | c.ayameClient = registerMessage.AyameClient 316 | c.environment = registerMessage.Environment 317 | c.libwebrtc = registerMessage.Libwebrtc 318 | 319 | // ログ出力 320 | c.signalingLog(*message, rawMessage) 321 | 322 | // Webhook 系のエラーログは Caller をつける 323 | resp, err := c.authnWebhook() 324 | if err != nil { 325 | c.errLog().Err(err).Caller().Msg("AuthnWebhookError") 326 | if err := c.sendRejectMessage("InternalServerError"); err != nil { 327 | c.errLog().Err(err).Caller().Msg("FailedSendRejectMessage") 328 | return err 329 | } 330 | return err 331 | } 332 | 333 | // 認証サーバの戻り値がおかしい場合は全部 Error にする 334 | if resp.Allowed == nil { 335 | c.errLog().Caller().Msg("AuthnWebhookResponseError") 336 | if err := c.sendRejectMessage("InternalServerError"); err != nil { 337 | c.errLog().Err(err).Caller().Msg("FailedSendRejectMessage") 338 | return err 339 | } 340 | return errAuthnWebhookResponse 341 | } 342 | 343 | if !*resp.Allowed { 344 | if resp.Reason == nil { 345 | c.errLog().Caller().Msg("AuthnWebhookResponseError") 346 | if err := c.sendRejectMessage("InternalServerError"); err != nil { 347 | c.errLog().Err(err).Caller().Msg("FailedSendRejectMessage") 348 | return err 349 | } 350 | return errAuthnWebhookResponse 351 | } 352 | if err := c.sendRejectMessage(*resp.Reason); err != nil { 353 | c.errLog().Err(err).Caller().Msg("FailedSendRejectMessage") 354 | return err 355 | } 356 | return errAuthnWebhookReject 357 | } 358 | 359 | c.authzMetadata = resp.AuthzMetadata 360 | 361 | // 戻り値は手抜き 362 | switch c.register() { 363 | case one: 364 | c.registered = true 365 | // room がまだなかった、accept を返す 366 | c.debugLog().Msg("REGISTERED-ONE") 367 | if err := c.sendAcceptMessage(false, resp.IceServers, resp.AuthzMetadata); err != nil { 368 | c.errLog().Err(err).Msg("FailedSendAcceptMessage") 369 | return err 370 | } 371 | case two: 372 | c.registered = true 373 | // room がすでにあって、一人いた、二人目 374 | c.debugLog().Msg("REGISTERED-TWO") 375 | if err := c.sendAcceptMessage(true, resp.IceServers, resp.AuthzMetadata); err != nil { 376 | c.errLog().Err(err).Msg("FailedSendAcceptMessage") 377 | return err 378 | } 379 | case full: 380 | // room が満杯だった 381 | c.errLog().Msg("RoomFilled") 382 | if err := c.sendRejectMessage("full"); err != nil { 383 | c.errLog().Err(err).Msg("FailedSendRejectMessage") 384 | return err 385 | } 386 | return errRoomFull 387 | } 388 | case "offer", "answer", "candidate": 389 | // register が完了していない 390 | if !c.registered { 391 | c.errLog().Msg("RegistrationIncomplete") 392 | return errRegistrationIncomplete 393 | } 394 | // ログ出力 395 | c.signalingLog(*message, rawMessage) 396 | c.forward(rawMessage) 397 | case "message": 398 | // 設定が有効でなければ default と同じ処理 399 | if !c.config.TypeMessage { 400 | c.errLog().Msg("InvalidMessageType") 401 | return errInvalidMessageType 402 | } 403 | 404 | // register が完了していない 405 | if !c.registered { 406 | c.errLog().Msg("RegistrationIncomplete") 407 | return errRegistrationIncomplete 408 | } 409 | // ログ出力 410 | c.signalingLog(*message, rawMessage) 411 | c.forward(rawMessage) 412 | case "connected": 413 | // register が完了していない 414 | if !c.registered { 415 | c.errLog().Msg("RegistrationIncomplete") 416 | return errRegistrationIncomplete 417 | } 418 | // TODO: c.standalone == false で type: connected を受信した場合はエラーにするか検討する 419 | if c.standalone { 420 | err := fmt.Errorf("WS-CONNECTED") 421 | c.errLog().Err(err).Send() 422 | return err 423 | } 424 | // ログ出力 425 | c.signalingLog(*message, rawMessage) 426 | default: 427 | c.errLog().Msg("InvalidMessageType") 428 | return errInvalidMessageType 429 | } 430 | return nil 431 | } 432 | 433 | func timerStop(timer *time.Timer) { 434 | // タイマー終了からのリセットへは以下参考にした 435 | // https://www.kaoriya.net/blog/2019/12/19/ 436 | if !timer.Stop() { 437 | select { 438 | case <-timer.C: 439 | default: 440 | } 441 | } 442 | } 443 | 444 | func getULID() string { 445 | t := time.Now() 446 | entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0) 447 | return ulid.MustNew(ulid.Timestamp(t), entropy).String() 448 | } 449 | -------------------------------------------------------------------------------- /connection_log.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "slices" 5 | 6 | "github.com/rs/zerolog" 7 | zlog "github.com/rs/zerolog/log" 8 | ) 9 | 10 | func (c *connection) signalingLog(message message, rawMessage []byte) { 11 | // signaling の type を指定してフィルターする 12 | if !slices.Contains(c.config.SignalingLogFilters, message.Type) { 13 | return 14 | } 15 | 16 | if c.config.Debug { 17 | c.signalingLogger.Debug(). 18 | Str("roomId", c.roomID). 19 | Str("clientId", c.clientID). 20 | Str("connectionId", c.ID). 21 | Str("type", message.Type). 22 | Str("rawMessage", string(rawMessage)). 23 | Send() 24 | return 25 | } 26 | 27 | c.signalingLogger.Info(). 28 | Str("roomId", c.roomID). 29 | Str("clientId", c.clientID). 30 | Str("connectionId", c.ID). 31 | Str("type", message.Type). 32 | Send() 33 | } 34 | 35 | func (c *connection) errLog() *zerolog.Event { 36 | return zlog.Error(). 37 | Str("roomId", c.roomID). 38 | Str("clientID", c.clientID). 39 | Str("connectionId", c.ID) 40 | } 41 | 42 | func (c *connection) debugLog() *zerolog.Event { 43 | return zlog.Debug(). 44 | Str("roomId", c.roomID). 45 | Str("clientID", c.clientID). 46 | Str("connectionId", c.ID) 47 | } 48 | -------------------------------------------------------------------------------- /disconnect_webhook.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/url" 7 | "time" 8 | ) 9 | 10 | type disconnectWebhookRequest struct { 11 | RoomID string `json:"roomId"` 12 | ClientID string `json:"clientId"` 13 | ConnectionID string `json:"connectionId"` 14 | } 15 | 16 | func (c *connection) disconnectWebhook() error { 17 | if c.config.DisconnectWebhookURL == "" { 18 | return nil 19 | } 20 | 21 | req := &disconnectWebhookRequest{ 22 | RoomID: c.roomID, 23 | ClientID: c.clientID, 24 | ConnectionID: c.ID, 25 | } 26 | 27 | start := time.Now() 28 | 29 | resp, err := c.postRequest(c.config.DisconnectWebhookURL, req) 30 | if err != nil { 31 | c.errLog().Err(err).Caller().Msg("DisconnectWebhookError") 32 | return errDisconnectWebhook 33 | } 34 | defer resp.Body.Close() 35 | 36 | c.webhookLog("disconnectReq", req) 37 | 38 | u, err := url.Parse(c.config.DisconnectWebhookURL) 39 | if err != nil { 40 | c.errLog().Err(err).Caller().Msg("DisconnectWebhookError") 41 | return errDisconnectWebhook 42 | } 43 | statusCode := fmt.Sprintf("%d", resp.StatusCode) 44 | m := c.metrics 45 | m.IncWebhookReqCnt(statusCode, "POST", u.Host, u.Path) 46 | m.ObserveWebhookReqDur(statusCode, "POST", u.Host, u.Path, time.Since(start).Seconds()) 47 | // TODO: ヘッダーのサイズも計測する 48 | m.ObserveWebhookReqSz(statusCode, "POST", u.Host, u.Path, resp.Request.ContentLength) 49 | m.ObserveWebhookResSz(statusCode, "POST", u.Host, u.Path, resp.ContentLength) 50 | 51 | body, err := io.ReadAll(resp.Body) 52 | if err != nil { 53 | c.errLog().Bytes("body", body).Err(err).Caller().Msg("DiconnectWebhookResponseError") 54 | return errDisconnectWebhookResponse 55 | } 56 | 57 | // ログ出力用 58 | httpResponse := &httpResponse{ 59 | Status: resp.Status, 60 | Proto: resp.Proto, 61 | Header: resp.Header, 62 | Body: string(body), 63 | } 64 | 65 | // 200 以外で返ってきたときはエラーとする 66 | if resp.StatusCode != 200 { 67 | c.errLog().Interface("resp", httpResponse).Caller().Msg("DisconnectWebhookUnexpectedStatusCode") 68 | return errDisconnectWebhookUnexpectedStatusCode 69 | } 70 | 71 | c.webhookLog("disconnectResp", httpResponse) 72 | 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /docs/DESIGN.md: -------------------------------------------------------------------------------- 1 | # Ayame 設計デザイン 2 | 3 | ## 設計 4 | 5 | - ログの時間は UTC に固定する 6 | - 切断が発生する場合のログレベルはエラー 7 | - 切断しないけどログに残しておきたいときはワーニング 8 | - Webhook 関連でエラーになった時はエラーをできるだけ詳細に出す 9 | - クライアント側でできるだけ処理をする 10 | - offer / answer / candidate は送られてきた JSON をそのまま転送する 11 | - forward の中身は connection, rawMessage を用意しておいてそれを書き込むだけにする 12 | - unregister の成功は forward チャネルのクローズをトリガーとする 13 | - チャネル利用時はインターフェースは使わない 14 | - 認証ウェブフックは register 前に行う 15 | - signalingKey のチェックなどもここで行う 16 | - 認証が成功したら register 処理を走らせる 17 | - その際にすでに他の人がマッチしてたらキックされる 18 | - 認証が成功したとしても接続できる状態になるとは限らない 19 | - 認証ウェブフックが ok になったら、register を試みる 20 | - 認証ウェブフックがなければそのまま処理に進む 21 | - 終了は丁寧に終了する 22 | - SDK サンプルがあるので、サンプルの提供はしない 23 | - main, wsRecv の2つのプロセスが動く 24 | - wsRecv はとにかく WS でメッセージを受け取って main にわたすだけ 25 | - wsRecv は main が死んだ時用に ctx を共有しておく 26 | - ping/pong は ping を 5 秒間隔でなげて 60 秒 pong が返ってこなかったら切断する 27 | - シグナリングの送受信ログを取る 28 | - roomId / clientId / connectionId を突っ込む 29 | - JSON 形式でとる 30 | - 送られてきたメッセージをそのまま書き出す 31 | - ログローテーションはすべてのログに共通にする 32 | - 個別には対応しない 33 | - 認証ウェブフックのログを取る 34 | - 送信、受信ログを取る 35 | - クライアント ID はオプション化する 36 | - 接続の名前付けはコネクション ID (ULID) を利用する 37 | - type: accept でコネクション ID を払い出す 38 | 39 | ## 利用ライブラリ 40 | 41 | - WS は gorilla/websocket 42 | - https://github.com/gorilla/websocket 43 | - ログは zerolog 44 | - https://github.com/rs/zerolog 45 | - ログローテは lumberjack 46 | - https://github.com/natefinch/lumberjack 47 | - コネクション ID は ulid 48 | - https://github.com/oklog/ulid 49 | 50 | ## 検討 51 | 52 | - API の利用を想定する 53 | - 指定した roomId を切断する 54 | - ConnectionID を UUIDv4 + Clockford Base32 に切り替える 55 | - ULID の意味があまりない 56 | -------------------------------------------------------------------------------- /docs/USE.md: -------------------------------------------------------------------------------- 1 | # Ayame を使ってみる 2 | 3 | ## セットアップ 4 | 5 | まずはこのリポジトリをクローンします。 6 | 7 | ### Go のインストール 8 | 9 | 推奨バージョンは以下のようになります。 10 | 11 | `go 1.24` 12 | 13 | ### パッケージのインストール 14 | 15 | ```bash 16 | go install 17 | ``` 18 | 19 | ### ビルドする 20 | 21 | ```bash 22 | make 23 | ``` 24 | 25 | ## 設定ファイルを生成する 26 | 27 | ```bash 28 | make init 29 | ``` 30 | 31 | ## サーバを起動する 32 | 33 | > [!WARNING] 34 | > デフォルトではコンソールにログは出力されません。 35 | 36 | ```bash 37 | ./bin/ayame -C config.ini 38 | ``` 39 | 40 | ## Ayame Web SDK サンプルを利用して動作確認をする 41 | 42 | Ayame Web SDK のサンプルを利用することで動作を確認できます。 43 | 44 | ```bash 45 | git clone git@github.com:OpenAyame/ayame-web-sdk-samples.git 46 | cd ayame-web-sdk-samples 47 | npm install 48 | ``` 49 | 50 | main.js URL を `'ws://127.0.0.1:3000/signaling'` に変更 51 | 52 | 53 | 54 | ```bash 55 | npm run dev 56 | ``` 57 | 58 | をブラウザタブで2つ開いて接続を押してみてください。 59 | 60 | ## コマンド 61 | 62 | ```bash 63 | $ ./bin/ayame -V 64 | WebRTC Signaling Server Ayame version 2025.2.0 65 | ``` 66 | 67 | ```bash 68 | $ ./bin/ayame -help 69 | Usage of ./bin/ayame: 70 | -c string 71 | ayame の設定ファイルへのパス(ini) (default "./config.ini") 72 | ``` 73 | 74 | ## `register` メッセージについて 75 | 76 | クライアントは ayame への接続可否を問い合わせるために WebSocket に接続した際に、まず `"type": "register"` の JSON メッセージを WS で送信する必要があります。 77 | register で送信できるプロパティは以下になります。 78 | 79 | - `"type"`: (string): 必須。 `"register"` を指定する 80 | - `"clientId"`: (string): 必須 81 | - `"roomId"`: (string): 必須 82 | - `"signalingkey"`(string): オプション 83 | - `"authnMetadata"`(object): オプション 84 | - `"ayameClient"`(string): オプション 85 | - `"environment"`(string): オプション 86 | - `"libwebrtc"`(string): オプション 87 | 88 | ## 認証ウェブフックの `authn_webhook_url` オプションについて 89 | 90 | `ayame.ini` にて `authn_webhook_url` を指定している場合、 91 | ayame は client が `{"type": "register" }` メッセージを送信してきた際に `config.ini` に指定した `authn_webhook_url` に対して認証リクエストを JSON 形式で POST します。 92 | 93 | また、 認証リクエストの返り値は JSON 形式で、以下のように想定されています。 94 | 95 | - `"allowed"`: (boolean): 必須。認証の可否 96 | - `"reason"`: (string): オプション。認証不可の際の理由 (`allowed` が false の場合のみ必須) 97 | - `"iceServers"`: (array object): オプション。クライアントに peer connection で接続する iceServer 情報 98 | 99 | `allowed` が false の場合 client の ayame への WebSocket 接続は切断されます。 100 | 101 | ### 認証ウェブフックリクエスト 102 | 103 | - `"clientId"`: (string): 必須 104 | - `"roomId"`: (string): 必須 105 | - `"signalingkey"`(string): オプション 106 | - `"authnMetadata"`: (object): オプション 107 | - register 時に `authnMetadata` をプロパティとして指定していると、その値がそのまま付与されます 108 | - `"ayameClient"`(string): オプション 109 | - `"environment"`(string): オプション 110 | - `"libwebrtc"`(string): オプション 111 | 112 | ### 認証ウェブフックレスポンス 113 | 114 | - `"allowed"`: (boolean): 必須。認証の可否 115 | - `"reason"`: (string): 認証不可の際の理由 (`allowed` が false の場合のみ) 116 | - `"authzMetadata"`(object): オプション 117 | - クライアントに対して任意に払い出せるメタデータ 118 | - client はこの値を読み込むことで、例えば username を認証サーバから送ったりということも可能になる 119 | 120 | ```json 121 | {"allowed": true, "authzMetadata": {"username": "ayame", "owner": "true"}} 122 | ``` 123 | 124 | ### ローカルで wss/https を試したい場合 125 | 126 | [ngrok \- secure introspectable tunnels to localhost](https://ngrok.com/) の使用を推奨しています。 127 | 128 | ```bash 129 | $ ngrok http 3000 130 | ngrok by @xxxxx 131 | Session Status online 132 | Account xxxxx 133 | Forwarding http://xxxxx.ngrok.io -> localhost:3000 134 | Forwarding https://xxxxx.ngrok.io -> localhost:3000 135 | ``` 136 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // メッセージはくっつける 8 | 9 | var ( 10 | errInvalidMessageType = errors.New("InvalidMessageType") 11 | errMissingRoomID = errors.New("MissingRoomID") 12 | errInvalidJSON = errors.New("InvalidJSON") 13 | errUnexpectedJSON = errors.New("UnexpectedJSON") 14 | 15 | errRegistrationIncomplete = errors.New("RegistrationIncomplete") 16 | 17 | errAuthnWebhook = errors.New("AuthnWebhookError") 18 | errAuthnWebhookResponse = errors.New("AuthnWebhookResponseError") 19 | errAuthnWebhookUnexpectedStatusCode = errors.New("AuthnWebhookUnexpectedStatusCode") 20 | errAuthnWebhookReject = errors.New("AuthnWebhookReject") 21 | 22 | errDisconnectWebhook = errors.New("DisconnectWebhookError") 23 | errDisconnectWebhookResponse = errors.New("DisconnectWebhookResponseError") 24 | errDisconnectWebhookUnexpectedStatusCode = errors.New("DisconnectWebhookUnexpectedStatusCode") 25 | 26 | errRoomFull = errors.New("RoomFull") 27 | // 想定外のエラー 28 | errInternalServer = errors.New("InternalServerError") 29 | ) 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenAyame/ayame 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.5.3 7 | github.com/labstack/echo-contrib v0.17.4 8 | github.com/labstack/echo/v4 v4.13.4 9 | github.com/oklog/ulid/v2 v2.1.1 10 | github.com/prometheus/client_golang v1.22.0 11 | github.com/rs/zerolog v1.34.0 12 | github.com/stretchr/testify v1.10.0 13 | golang.org/x/sync v0.14.0 14 | gopkg.in/ini.v1 v1.67.0 15 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 16 | ) 17 | 18 | require ( 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/kr/text v0.2.0 // indirect 23 | github.com/labstack/gommon v0.4.2 // indirect 24 | github.com/mattn/go-colorable v0.1.14 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/prometheus/client_model v0.6.2 // indirect 29 | github.com/prometheus/common v0.63.0 // indirect 30 | github.com/prometheus/procfs v0.16.1 // indirect 31 | github.com/valyala/bytebufferpool v1.0.0 // indirect 32 | github.com/valyala/fasttemplate v1.2.2 // indirect 33 | golang.org/x/crypto v0.38.0 // indirect 34 | golang.org/x/net v0.40.0 // indirect 35 | golang.org/x/sys v0.33.0 // indirect 36 | golang.org/x/text v0.25.0 // indirect 37 | golang.org/x/time v0.11.0 // indirect 38 | google.golang.org/protobuf v1.36.6 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 6 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 10 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 11 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 12 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 13 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 14 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 15 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 16 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 17 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 18 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 19 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 20 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 21 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 22 | github.com/labstack/echo-contrib v0.17.3 h1:hj+qXksKZG1scSe9ksUXMtv7fZYN+PtQT+bPcYA3/TY= 23 | github.com/labstack/echo-contrib v0.17.3/go.mod h1:TcRBrzW8jcC4JD+5Dc/pvOyAps0rtgzj7oBqoR3nYsc= 24 | github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= 25 | github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= 26 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 27 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 28 | github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= 29 | github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= 30 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 31 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 32 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 33 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 34 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 35 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 36 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 37 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 38 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 39 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 40 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 41 | github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= 42 | github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= 43 | github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= 44 | github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= 45 | github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= 46 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 47 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 48 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 49 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 50 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 51 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 52 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 53 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 54 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 55 | github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= 56 | github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= 57 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 58 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 59 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 60 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 61 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 62 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 63 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 67 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 68 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 69 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 70 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 71 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 72 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 73 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 74 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 75 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 76 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 77 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 78 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 79 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 80 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 81 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 82 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 86 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 87 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 88 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 89 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 90 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 91 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 92 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 93 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 94 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 95 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 96 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 97 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 98 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 99 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 100 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 101 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 102 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 103 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 104 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 105 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 106 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/rs/zerolog" 11 | "gopkg.in/natefinch/lumberjack.v2" 12 | ) 13 | 14 | func InitLogger(config *Config) error { 15 | // https://github.com/rs/zerolog/issues/77 16 | zerolog.TimestampFunc = func() time.Time { 17 | return time.Now().UTC() 18 | } 19 | 20 | zerolog.TimeFieldFormat = time.RFC3339Nano 21 | 22 | if config.LogMessageKeyName != "" { 23 | zerolog.MessageFieldName = config.LogMessageKeyName 24 | } 25 | if config.LogTimestampKeyName != "" { 26 | zerolog.TimestampFieldName = config.LogTimestampKeyName 27 | } 28 | 29 | if config.Debug { 30 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 31 | } else { 32 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func NewLogger(config *Config, logFilename string, logDomain string) (*zerolog.Logger, error) { 39 | // デバッグコンソールログを出力する 40 | // デバッグコンソールには Caller を出力する 41 | if config.Debug && config.DebugConsoleLog { 42 | // デバッグコンソールを JSON 形式で出力 43 | if config.DebugConsoleLogJSON { 44 | logger := zerolog.New(os.Stdout).With().Caller().Timestamp().Str("domain", logDomain).Logger() 45 | return &logger, nil 46 | } 47 | 48 | // デバッグコンソールをヒューマンリーダブルな形式で出力 49 | writer := zerolog.ConsoleWriter{ 50 | Out: os.Stdout, 51 | FormatTimestamp: func(i interface{}) string { 52 | darkGray := "\x1b[90m" 53 | reset := "\x1b[0m" 54 | return strings.Join([]string{darkGray, i.(string), reset}, "") 55 | }, 56 | NoColor: false, 57 | } 58 | prettyFormat(&writer) 59 | logger := zerolog.New(writer).With().Caller().Timestamp().Str("domain", logDomain).Logger() 60 | 61 | return &logger, nil 62 | } 63 | 64 | // 標準出力にログを出力する 65 | if config.LogStdout { 66 | logger := zerolog.New(os.Stdout).With().Timestamp().Str("domain", logDomain).Logger() 67 | return &logger, nil 68 | } 69 | 70 | if f, err := os.Stat(config.LogDir); os.IsNotExist(err) || !f.IsDir() { 71 | return nil, err 72 | } 73 | 74 | // ログファイルを出力する 75 | logPath := fmt.Sprintf("%s/%s", config.LogDir, logFilename) 76 | 77 | lumberjackLogger := &lumberjack.Logger{ 78 | Filename: logPath, 79 | MaxSize: config.LogRotateMaxSize, 80 | MaxBackups: config.LogRotateMaxBackups, 81 | MaxAge: config.LogRotateMaxAge, 82 | Compress: config.LogRotateCompress, 83 | } 84 | logger := zerolog.New(lumberjackLogger).With().Timestamp().Str("domain", logDomain).Logger() 85 | 86 | return &logger, nil 87 | } 88 | 89 | // 現時点での prettyFormat 90 | // 2023-04-17 12:51:56.333485Z [INFO] config.go:102 > CONF | debug=true 91 | func prettyFormat(w *zerolog.ConsoleWriter) { 92 | const Reset = "\x1b[0m" 93 | 94 | w.FormatLevel = func(i interface{}) string { 95 | var color, level string 96 | // TODO: 各色を定数に置き換える 97 | // TODO: 他の logLevel が必要な場合は追加する 98 | 99 | switch i.(string) { 100 | case "info": 101 | color = "\x1b[32m" 102 | case "error": 103 | color = "\x1b[31m" 104 | case "warn": 105 | color = "\x1b[33m" 106 | case "debug": 107 | color = "\x1b[34m" 108 | default: 109 | color = "\x1b[37m" 110 | } 111 | 112 | level = strings.ToUpper(i.(string)) 113 | return fmt.Sprintf("%s[%s]%s", color, level, Reset) 114 | } 115 | w.FormatCaller = func(i interface{}) string { 116 | return fmt.Sprintf("[%s]", filepath.Base(i.(string))) 117 | } 118 | // TODO: Caller をファイル名と行番号だけの表示で出力する 119 | // 以下のようなフォーマットにしたい 120 | // 2023-04-17 12:50:09.334758Z [INFO] [config.go:102] CONF | debug=true 121 | // TODO: name=value が無い場合に | を消す方法がわからなかった 122 | w.FormatMessage = func(i interface{}) string { 123 | if i == nil || i == "" { 124 | return "" 125 | } 126 | return fmt.Sprintf("%s |", i) 127 | } 128 | w.FormatFieldName = func(i interface{}) string { 129 | const Cyan = "\x1b[36m" 130 | return fmt.Sprintf("%s%s=%s", Cyan, i, Reset) 131 | } 132 | // TODO: カンマ区切りを同実現するかわからなかった 133 | w.FormatFieldValue = func(i interface{}) string { 134 | return fmt.Sprintf("%s", i) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /messages.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | const ( 4 | // 登録成功 5 | // 部屋が作成された相手を待つ 6 | one int = iota 7 | // すでに部屋はあり相手が待ってる 8 | // offer を出させる 9 | // 登録成功 10 | two 11 | // 満員だったので Reject か Error を返す 12 | // 登録失敗 13 | full 14 | ) 15 | 16 | type register struct { 17 | connection *connection 18 | resultChannel chan int 19 | } 20 | 21 | // rawMessage には JOSN パース前の offer / answer / candidate が入る 22 | type forward struct { 23 | connection *connection 24 | rawMessage []byte 25 | } 26 | 27 | type unregister struct { 28 | connection *connection 29 | } 30 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/labstack/echo-contrib/prometheus" 7 | prom "github.com/prometheus/client_golang/prometheus" 8 | ) 9 | 10 | const ( 11 | KB = prometheus.KB 12 | MB = prometheus.MB 13 | 14 | MetricsKey = "webhook_metrics" 15 | ) 16 | 17 | var ( 18 | webhookReqDurBuckets = prom.DefBuckets 19 | webhookReqSzBuckets = []float64{1.0 * KB, 2.0 * KB, 5.0 * KB, 10.0 * KB, 100 * KB, 500 * KB, 1.0 * MB, 2.5 * MB, 5.0 * MB, 10.0 * MB} 20 | webhookResSzBuckets = []float64{1.0 * KB, 2.0 * KB, 5.0 * KB, 10.0 * KB, 100 * KB, 500 * KB, 1.0 * MB, 2.5 * MB, 5.0 * MB, 10.0 * MB} 21 | ) 22 | 23 | var ( 24 | webhookReqCnt = &prometheus.Metric{ 25 | ID: "webhookReqCnt", 26 | Name: "webhook_requests_total", 27 | Description: "How many HTTP requests.", 28 | Type: "counter_vec", 29 | Args: []string{"code", "method", "host", "url"}} 30 | webhookReqDur = &prometheus.Metric{ 31 | ID: "webhookReqDur", 32 | Name: "webhook_request_duration_seconds", 33 | Description: "The HTTP request latencies in seconds.", 34 | Args: []string{"code", "method", "host", "url"}, 35 | Type: "histogram_vec", 36 | Buckets: webhookReqDurBuckets} 37 | webhookReqSz = &prometheus.Metric{ 38 | ID: "webhookReqSz", 39 | Name: "webhook_request_message_size_bytes", 40 | Description: "The HTTP request message sizes in bytes.", 41 | Args: []string{"code", "method", "host", "url"}, 42 | Type: "histogram_vec", 43 | Buckets: webhookReqSzBuckets} 44 | webhookResSz = &prometheus.Metric{ 45 | ID: "webhookResSz", 46 | Name: "webhook_response_message_size_bytes", 47 | Description: "The HTTP response message sizes in bytes.", 48 | Args: []string{"code", "method", "host", "url"}, 49 | Type: "histogram_vec", 50 | Buckets: webhookResSzBuckets} 51 | authnWebhookCnt = &prometheus.Metric{ 52 | ID: "authnWebhookRespCnt", 53 | Name: "authn_webhook_responses_total", 54 | Description: "How many AuthnWebhook responses.", 55 | Type: "counter_vec", 56 | Args: []string{"code", "method", "host", "url", "allowed", "reason"}} 57 | 58 | metricsList = []*prometheus.Metric{ 59 | webhookReqCnt, 60 | webhookReqDur, 61 | webhookResSz, 62 | webhookReqSz, 63 | authnWebhookCnt, 64 | } 65 | ) 66 | 67 | type Metrics struct { 68 | WebhookReqCnt *prometheus.Metric 69 | WebhookReqDur *prometheus.Metric 70 | WebhookResSz *prometheus.Metric 71 | WebhookReqSz *prometheus.Metric 72 | AuthnWebhookCnt *prometheus.Metric 73 | } 74 | 75 | func NewMetrics() *Metrics { 76 | return &Metrics{ 77 | WebhookReqCnt: webhookReqCnt, 78 | WebhookReqDur: webhookReqDur, 79 | WebhookResSz: webhookResSz, 80 | WebhookReqSz: webhookReqSz, 81 | AuthnWebhookCnt: authnWebhookCnt, 82 | } 83 | } 84 | 85 | func (m *Metrics) IncWebhookReqCnt(code, method, host, url string) { 86 | labels := prom.Labels{ 87 | "code": code, 88 | "method": method, 89 | "host": host, 90 | "url": url, 91 | } 92 | m.WebhookReqCnt.MetricCollector.(*prom.CounterVec).With(labels).Inc() 93 | } 94 | 95 | func (m *Metrics) ObserveWebhookReqDur(code, method, host, url string, elapsed float64) { 96 | labels := prom.Labels{ 97 | "code": code, 98 | "method": method, 99 | "host": host, 100 | "url": url, 101 | } 102 | m.WebhookReqDur.MetricCollector.(*prom.HistogramVec).With(labels).Observe(elapsed) 103 | } 104 | 105 | func (m *Metrics) ObserveWebhookResSz(code, method, host, url string, sz int64) { 106 | labels := prom.Labels{ 107 | "code": code, 108 | "method": method, 109 | "host": host, 110 | "url": url, 111 | } 112 | m.WebhookResSz.MetricCollector.(*prom.HistogramVec).With(labels).Observe(float64(sz)) 113 | } 114 | 115 | func (m *Metrics) ObserveWebhookReqSz(code, method, host, url string, sz int64) { 116 | labels := prom.Labels{ 117 | "code": code, 118 | "method": method, 119 | "host": host, 120 | "url": url, 121 | } 122 | m.WebhookReqSz.MetricCollector.(*prom.HistogramVec).With(labels).Observe(float64(sz)) 123 | } 124 | 125 | func (m *Metrics) IncAuthnWebhookCnt(code, method, host, url string, allowed bool, reason string) { 126 | labels := prom.Labels{ 127 | "code": code, 128 | "method": method, 129 | "host": host, 130 | "url": url, 131 | "allowed": fmt.Sprintf("%v", allowed), 132 | "reason": reason, 133 | } 134 | m.AuthnWebhookCnt.MetricCollector.(*prom.CounterVec).With(labels).Inc() 135 | } 136 | -------------------------------------------------------------------------------- /ok_handler.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func (s *Server) okHandler(c echo.Context) error { 10 | return c.NoContent(http.StatusOK) 11 | } 12 | -------------------------------------------------------------------------------- /ok_handler_test.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestOkHandler(t *testing.T) { 13 | e := echo.New() 14 | 15 | s := &Server{ 16 | Server: http.Server{ 17 | Handler: e, 18 | }, 19 | } 20 | 21 | req := httptest.NewRequest(http.MethodGet, "/.ok", nil) 22 | rec := httptest.NewRecorder() 23 | c := e.NewContext(req, rec) 24 | 25 | if assert.NoError(t, s.okHandler(c)) { 26 | assert.Equal(t, http.StatusOK, rec.Code) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /room.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import "context" 4 | 5 | var ( 6 | // register/unregister は待たせる 7 | registerChannel = make(chan *register) 8 | unregisterChannel = make(chan *unregister) 9 | // ブロックされたくないので 100 に設定 10 | forwardChannel = make(chan forward, 100) 11 | ) 12 | 13 | // roomId がキーになる 14 | type room struct { 15 | connections map[string]*connection 16 | } 17 | 18 | func (s *Server) StartMatchServer(ctx context.Context) error { 19 | // room を管理するマップはここに用意する 20 | var m = make(map[string]room) 21 | // ここはシングルなのでロックは不要 22 | for { 23 | select { 24 | case <-ctx.Done(): 25 | return ctx.Err() 26 | case register := <-registerChannel: 27 | c := register.connection 28 | rch := register.resultChannel 29 | r, ok := m[c.roomID] 30 | if ok { 31 | // room があった 32 | if len(r.connections) == 1 { 33 | r.connections[c.ID] = c 34 | m[c.roomID] = r 35 | rch <- two 36 | } else { 37 | // room あったけど満杯 38 | rch <- full 39 | } 40 | } else { 41 | // room がなかった 42 | var connections = make(map[string]*connection) 43 | connections[c.ID] = c 44 | // room を追加 45 | m[c.roomID] = room{ 46 | connections: connections, 47 | } 48 | c.debugLog().Msg("CREATED-ROOM") 49 | rch <- one 50 | } 51 | case unregister := <-unregisterChannel: 52 | c := unregister.connection 53 | // room を探す 54 | r, ok := m[c.roomID] 55 | // room がない場合は何もしない 56 | if ok { 57 | _, ok := r.connections[c.ID] 58 | if ok { 59 | for _, connection := range r.connections { 60 | // 両方の forwardChannel を閉じる 61 | close(connection.forwardChannel) 62 | connection.debugLog().Msg("CLOSED-FORWARD-CHANNEL") 63 | connection.debugLog().Msg("REMOVED-CLIENT") 64 | } 65 | // room を削除 66 | delete(m, c.roomID) 67 | c.debugLog().Msg("DELETED-ROOM") 68 | } 69 | } 70 | case forward := <-forwardChannel: 71 | r, ok := m[forward.connection.roomID] 72 | // room がない場合は何もしない 73 | if ok { 74 | // room があった 75 | for connectionID, client := range r.connections { 76 | // 自分ではない方に投げつける 77 | if connectionID != forward.connection.ID { 78 | client.forwardChannel <- forward 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/labstack/echo-contrib/prometheus" 10 | "github.com/labstack/echo/v4" 11 | "github.com/rs/zerolog" 12 | zlog "github.com/rs/zerolog/log" 13 | ) 14 | 15 | type Server struct { 16 | config *Config 17 | 18 | signalingLogger *zerolog.Logger 19 | webhookLogger *zerolog.Logger 20 | 21 | EchoPrometheus *echo.Echo 22 | Metrics *Metrics 23 | 24 | http.Server 25 | } 26 | 27 | func NewServer(config *Config) (*Server, error) { 28 | // signaling.jsonl ロガーを用意 29 | signalingLogger, err := NewLogger(config, config.SignalingLogName, "signaling") 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // webhook.jsonl ロガーを用意 35 | webhookLogger, err := NewLogger(config, config.WebhookLogName, "webhook") 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | e := echo.New() 41 | 42 | // URL の生成 43 | url := fmt.Sprintf("%s:%d", config.ListenIPv4Address, config.ListenPortNumber) 44 | 45 | s := &Server{ 46 | config: config, 47 | signalingLogger: signalingLogger, 48 | webhookLogger: webhookLogger, 49 | Server: http.Server{ 50 | Addr: url, 51 | ReadHeaderTimeout: readHeaderTimeout, 52 | Handler: e, 53 | }, 54 | } 55 | 56 | // websocket server 57 | e.GET("/signaling", s.signalingHandler) 58 | e.GET("/.ok", s.okHandler) 59 | 60 | echoPrometheus := echo.New() 61 | echoPrometheus.HideBanner = true 62 | echoPrometheus.HidePort = true 63 | 64 | p := prometheus.NewPrometheus("ayame", nil, metricsList) 65 | e.Use(p.HandlerFunc) 66 | p.SetMetricsPath(echoPrometheus) 67 | 68 | s.EchoPrometheus = echoPrometheus 69 | 70 | s.Metrics = NewMetrics() 71 | 72 | return s, nil 73 | } 74 | 75 | const readHeaderTimeout = 10 * time.Second 76 | 77 | func (s *Server) Start(ctx context.Context) error { 78 | ch := make(chan error) 79 | go func() { 80 | defer close(ch) 81 | if err := s.ListenAndServe(); err != nil { 82 | ch <- err 83 | } 84 | }() 85 | 86 | defer func() { 87 | if err := s.Shutdown(ctx); err != nil { 88 | zlog.Error().Err(err).Send() 89 | } 90 | }() 91 | 92 | select { 93 | case <-ctx.Done(): 94 | return ctx.Err() 95 | case err := <-ch: 96 | return err 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /signaling_handler.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/labstack/echo/v4" 10 | zlog "github.com/rs/zerolog/log" 11 | ) 12 | 13 | const ( 14 | writeWait = 10 * time.Second 15 | 16 | // ws の読み込みは最大 1MByte までにする 17 | readLimit = 1048576 18 | ) 19 | 20 | var ( 21 | upgrader = websocket.Upgrader{ 22 | ReadBufferSize: 1024 * 4, 23 | WriteBufferSize: 1024 * 4, 24 | CheckOrigin: func(r *http.Request) bool { 25 | return true 26 | }, 27 | } 28 | ) 29 | 30 | func (s *Server) signalingHandler(c echo.Context) error { 31 | r := c.Request() 32 | w := c.Response() 33 | 34 | wsConn, err := upgrader.Upgrade(w, r, nil) 35 | if err != nil { 36 | zlog.Debug().Err(err).Send() 37 | return err 38 | } 39 | 40 | wsConn.SetReadLimit(readLimit) 41 | 42 | // s.config.CopyWebSocketHeaderNames にマッチするヘッダーを取得して connection.copyHeaders に設定する 43 | copyHeaders := make(map[string]string) 44 | for _, headerName := range s.config.CopyWebSocketHeaderNames { 45 | // 設定ファイルのヘッダー名を http.CanonicalHeaderKey で正規化してから Header.Get で取得する 46 | canonicalName := http.CanonicalHeaderKey(headerName) 47 | if headerValue := r.Header.Get(canonicalName); headerValue != "" { 48 | copyHeaders[canonicalName] = headerValue 49 | } 50 | } 51 | 52 | connection := connection{ 53 | // connectionId を設定する 54 | ID: getULID(), 55 | 56 | wsConn: wsConn, 57 | // 複数箇所でブロックした時を考えて少し余裕をもたせる 58 | forwardChannel: make(chan forward, 100), 59 | 60 | // config を connection でも触れるように渡しておく 61 | config: *s.config, 62 | signalingLogger: *s.signalingLogger, 63 | webhookLogger: *s.webhookLogger, 64 | metrics: s.Metrics, 65 | 66 | copyHeaders: copyHeaders, 67 | } 68 | 69 | // client.conn.SetCloseHandler(client.closeHandler) 70 | ctx := context.Background() 71 | ctx, cancel := context.WithCancel(ctx) 72 | 73 | // ブロックしないよう余裕をもたせておく 74 | messageChannel := make(chan []byte, 100) 75 | go connection.wsRecv(ctx, messageChannel) 76 | go connection.main(cancel, messageChannel) 77 | 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /signaling_handler_test.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSignalingHandler(t *testing.T) { 13 | t.Skip("") 14 | } 15 | 16 | func TestSignalingHandlerNoWebSocketHeaders(t *testing.T) { 17 | e := echo.New() 18 | 19 | s := &Server{ 20 | Server: http.Server{ 21 | Handler: e, 22 | }, 23 | } 24 | 25 | req := httptest.NewRequest(http.MethodGet, "/signaling", nil) 26 | //req.Header.Set("connection", "upgrade") 27 | //req.Header.Set("upgrade", "websocket") 28 | //req.Header.Set("Sec-WebSocket-Version", "13") 29 | //req.Header.Set("Sec-WebSocket-Key", "9KSB2xxx0G/vaPz4e5+ACw==") 30 | rec := httptest.NewRecorder() 31 | c := e.NewContext(req, rec) 32 | 33 | assert.Error(t, s.signalingHandler(c)) 34 | assert.Equal(t, http.StatusBadRequest, rec.Code) 35 | } 36 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all", "-ST1000"] -------------------------------------------------------------------------------- /webhook.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | type httpResponse struct { 11 | Status string `json:"status"` 12 | Proto string `json:"proto"` 13 | Header http.Header `json:"header"` 14 | Body string `json:"body"` 15 | } 16 | 17 | // JSON HTTP Request をするだけのラッパー 18 | func (c *connection) postRequest(u string, body interface{}) (*http.Response, error) { 19 | reqJSON, err := json.Marshal(body) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | req, err := http.NewRequest( 25 | "POST", 26 | u, 27 | bytes.NewBuffer([]byte(reqJSON)), 28 | ) 29 | if err != nil { 30 | return nil, err 31 | } 32 | req.Header.Set("Content-Type", "application/json") 33 | 34 | // シグナリングからコピーした HTTP ヘッダーを設定する 35 | for k, v := range c.copyHeaders { 36 | req.Header.Set(k, v) 37 | } 38 | 39 | timeout := time.Duration(c.config.WebhookRequestTimeoutSec) * time.Second 40 | 41 | client := &http.Client{Timeout: timeout} 42 | return client.Do(req) 43 | } 44 | 45 | func (c *connection) webhookLog(n string, v interface{}) { 46 | c.webhookLogger.Log(). 47 | Str("roomId", c.roomID). 48 | Str("clientId", c.ID). 49 | Interface("copyHeaders", c.copyHeaders). 50 | Interface(n, v). 51 | Send() 52 | } 53 | -------------------------------------------------------------------------------- /ws_messages.go: -------------------------------------------------------------------------------- 1 | package ayame 2 | 3 | // Type を確認する用 4 | type message struct { 5 | Type string `json:"type"` 6 | } 7 | 8 | type registerMessage struct { 9 | Type string `json:"type" binding:"required"` 10 | RoomID string `json:"roomId" binding:"required"` 11 | ClientID string `json:"clientId"` 12 | AuthnMetadata *interface{} `json:"authnMetadata"` 13 | SignalingKey *string `json:"signalingKey"` 14 | // 後方互換性対応 15 | Key *string `json:"key"` 16 | // Ayame クライアント情報が詰まっている 17 | AyameClient *string `json:"ayameClient"` 18 | Libwebrtc *string `json:"libwebrtc"` 19 | Environment *string `json:"environment"` 20 | Standalone bool `json:"standalone"` 21 | } 22 | 23 | type pingMessage struct { 24 | Type string `json:"type"` 25 | } 26 | 27 | type byeMessage struct { 28 | Type string `json:"type"` 29 | } 30 | 31 | type acceptMessage struct { 32 | Type string `json:"type"` 33 | ConnectionID string `json:"connectionId"` 34 | // WaitingOffer bool `json:"waitingOffer"` 35 | AuthzMetadata *interface{} `json:"authzMetadata,omitempty"` 36 | IceServers *[]iceServer `json:"iceServers,omitempty"` 37 | 38 | // 後方互換性対応 39 | IsExistClient bool `json:"isExistClient"` 40 | // 後方互換性対応 41 | IsExistUser bool `json:"isExistUser"` 42 | } 43 | 44 | type rejectMessage struct { 45 | Type string `json:"type"` 46 | Reason string `json:"reason"` 47 | } 48 | 49 | type iceServer struct { 50 | Urls []string `json:"urls"` 51 | UserName *string `json:"username,omitempty"` 52 | Credential *string `json:"credential,omitempty"` 53 | } 54 | --------------------------------------------------------------------------------