Compare commits

...
81 Commits
Author SHA1 Message Date
Owncast f1da0fc30c Bundle embedded web app 2026-04-11 20:47:29 +00:00
renovate[bot]andGitHub 1160fc3e9f chore(deps): update dependency msw-storybook-addon to v2.0.7 2026-04-11 16:57:23 +00:00
Owncast 5c5f678523 Bundle embedded web app 2026-04-11 16:55:56 +00:00
renovate[bot]andGitHub 92da6edd93 chore(deps): update dependency msw to v2.13.2 2026-04-11 12:54:16 +00:00
Owncast cb73028e51 Bundle embedded web app 2026-04-11 09:28:14 +00:00
renovate[bot]andGitHub c207815b8e chore(deps): update typescript-eslint monorepo to v8.58.1 2026-04-11 05:56:21 +00:00
Owncast 875b41dd99 Bundle embedded web app 2026-04-11 05:55:12 +00:00
renovate[bot]andGitHub 1a578bab2a chore(deps): update dependency msw to v2.13.1 2026-04-11 02:02:55 +00:00
Owncast 415657298f Bundle embedded web app 2026-04-11 02:00:28 +00:00
renovate[bot]andGitHub 8c5dc5f3b2 chore(deps): update dependency cypress to v15.13.1 2026-04-10 22:15:43 +00:00
Gabe Kangas 05a3e8696d fix(ci): the auto-comment on missing checklist wasn't firing 2026-04-10 10:25:04 -07:00
renovate[bot]andGitHub 09b4179b73 fix(deps): update module golang.org/x/crypto to v0.50.0 2026-04-09 19:01:40 +00:00
Owncast 92689ffd6b Bundle embedded web app 2026-04-09 18:58:55 +00:00
renovate[bot]andGitHub 432079d660 chore(deps): update dependency msw to v2.13.0 2026-04-09 15:06:27 +00:00
renovate[bot]andGitHub 0cbf8fad1b fix(deps): update module github.com/oapi-codegen/runtime to v1.4.0 2026-04-09 01:58:48 +00:00
renovate[bot]andGitHub bf4636b148 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.42 2026-04-08 21:14:07 +00:00
renovate[bot]andGitHub 43630aaccf chore(deps): update dependency go to v1.26.2 2026-04-08 04:58:58 +00:00
renovate[bot]andGitHub 93379cbc08 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.41 2026-04-06 16:57:45 +00:00
Owncast 153f9a488f Bundle embedded web app 2026-04-06 05:37:13 +00:00
renovate[bot]andGitHub 8c9cf367ea chore(deps): update dependency sass to v1.99.0 2026-04-06 01:57:36 +00:00
Owncast a42f0ff5a5 Bundle embedded web app 2026-04-05 20:53:08 +00:00
renovate[bot]andGitHub d9d26035aa chore(deps): update dependency chromatic to v16.1.0 2026-04-05 17:31:26 +00:00
Owncast 3209695726 Bundle embedded web app 2026-04-04 21:18:10 +00:00
renovate[bot]andGitHub 97f91f1c87 chore(deps): update dependency mermaid to v11.14.0 2026-04-04 20:37:42 +00:00
Owncast 6ee9c6cd99 Bundle embedded web app 2026-04-04 20:36:08 +00:00
renovate[bot]andGitHub 20e6d5cb5e chore(deps): update dependency ts-jest to v29.4.9 2026-04-04 17:04:22 +00:00
Owncast e699bef636 Bundle embedded web app
Lint / GitHub actions (push) Successful in 13s
Automated API tests / test (push) Failing after 7s
Browser Tests / cypress-run (push) Failing after 6s
Chromatic / chromatic-deployment (push) Failing after 4s
CodeQL / Analyze (go) (push) Failing after 58s
CodeQL / Analyze (javascript) (push) Failing after 47s
Build development container / Earthly (push) Successful in 29s
CSS Lint and Formatting / css-lint (push) Successful in 17s
Lint / Go linter (push) Failing after 6s
HLS tests / tests (push) Failing after 6s
Javascript / Code formatting (push) Failing after 6s
Javascript / Test for unused code (push) Failing after 5s
Javascript / Build and bundle web project (push) Has been skipped
Go Tests / test (push) Has been cancelled
Go Tests / test-bsds (map[name:freebsd version:12.2]) (push) Has been cancelled
Go Tests / test-bsds (map[name:openbsd version:6.8]) (push) Has been cancelled
2026-04-04 17:02:24 +00:00
renovate[bot]andGitHub 854674b5fb fix(deps): update dependency react-virtuoso to v4.18.4 2026-04-04 09:19:26 +00:00
renovate[bot]andGitHub 873d7e0a5f fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.40 2026-04-03 16:45:09 +00:00
Gabe Kangas 519d239a87 chore(ci): post a comment if the PR template is invalid 2026-04-03 09:43:16 -07:00
renovate[bot]andGitHub c13eaf25dd fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.39 2026-04-03 05:05:51 +00:00
Owncast 324bf75f69 Bundle embedded web app 2026-04-02 21:57:22 +00:00
renovate[bot]andGitHub 2c64efe505 chore(deps): update typescript-eslint monorepo to v8.58.0 2026-04-02 17:48:38 +00:00
Owncast 722226e054 Bundle embedded web app 2026-04-02 17:46:37 +00:00
renovate[bot]andGitHub d10fc7f5d4 fix(deps): update dependency lodash to v4.18.1 [security] 2026-04-02 13:58:14 +00:00
601553c582 chore(i18n): update translations from Crowdin (#4862)
Co-authored-by: Owncast <owncast@owncast.online>
2026-04-01 11:39:48 -07:00
renovate[bot]andGitHub 8b5fe84e62 chore(deps): update peter-evans/create-or-update-comment digest to 5723223 2026-04-01 09:42:10 +00:00
33efafecd5 feat: implement custom accessible tooltips with keyboard shortcuts for the video player (#4787)
* feat(video-player): add keyboard shortcut tooltips to control bar

- Display keyboard shortcuts (e.g., "Play (Space)") in button tooltips.
- Inject shortcut suffixes into Video.js language strings dynamically.
- Add CSS to style `.vjs-control-text` as a floating tooltip on hover.
- Enable `noUITitleAttributes` to disable native browser tooltips and prevent duplication.

* fix(player-control-tooltips): Prevent player control tooltips from displaying incorrectly in mobile

- Implement mobile touch detection to set 'player-data-is-touch' attribute on body, preventing sticky hover states.

* chore(linting): fix linting and styling errors in VideoJS

* Improve hover detection for tooltips

---------

Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2026-03-31 20:38:03 -07:00
John CostaandGitHub 8ccec822b4 fix(web): set page title on chat embed pages (#4820)
Set a translated page title on the readonly and readwrite chat embed
pages so popout/embedded chat windows display a meaningful title
based on the configured stream name, addressing #4794.

Uses next-export-i18n with a new chatEmbedTitle key for proper
localization. Also fixes the readonly embed error boundary
componentName from ReadWriteChatEmbed to ReadOnlyChatEmbed.
2026-03-31 20:36:28 -07:00
Gabe KangasandGitHub e738156fd7 Sanitize actor displaynames (#4864)
* Sanitize AP name possible XSS

* Pull out sanitizing method + remove rendering markdown for AP chat messages

* Add tests around AP message rendering and usernames

* Add sanitization tests to AP integration test
2026-03-31 20:33:05 -07:00
Gabe Kangas a2981903c2 Move activitypub automated test to run under a container 2026-03-30 12:32:04 -07:00
renovate[bot]andGitHub 5d4fcf9686 fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.38 2026-03-30 05:36:32 +00:00
Owncast 50854f09b3 Bundle embedded web app 2026-03-29 02:02:57 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
54104a5c3c fix(deps): update dependency @uiw/react-codemirror to v4.25.9 (#4859)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-29 01:59:06 +00:00
Owncast 715c267079 Bundle embedded web app 2026-03-28 21:51:18 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
c48cc989f4 fix(deps): update dependency @uiw/codemirror-theme-bbedit to v4.25.9 (#4858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 21:47:19 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
44970293f8 fix(deps): update module github.com/oapi-codegen/runtime to v1.3.1 (#4852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-28 01:26:54 +00:00
Owncast 423ddb8e22 Bundle embedded web app 2026-03-28 00:21:50 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c593ed381d chore(deps-dev): bump picomatch in /test/automated/api (#4850)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:08:17 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b6a39721dd chore(deps-dev): bump picomatch in /test/automated/hls (#4848)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:07:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
159e24438d chore(deps-dev): bump flatted from 3.3.3 to 3.4.2 in /web (#4836)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:07:21 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2c3e8fa3c1 chore(deps-dev): bump handlebars from 4.7.8 to 4.7.9 in /web (#4854)
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.9
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:06:57 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ba8ce8bf66 chore(deps-dev): bump brace-expansion from 1.1.12 to 1.1.13 in /web (#4856)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:06:21 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
84341875b7 chore(deps): bump brace-expansion in /test/load (#4857)
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.2...v2.0.3)

Updates `brace-expansion` from 1.1.12 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v2.0.2...v2.0.3)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 2.0.3
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 17:06:01 -07:00
Owncast d52911160d Bundle embedded web app 2026-03-27 23:45:57 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4ef036b0f4 chore(deps): update dependency chromatic to v16 (#4861)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 16:37:17 -07:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
eb3184bcfa chore(deps): update chromaui/action action to v16 (#4860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 16:37:00 -07:00
Gabe Kangas 5c9b2333f9 fix(i18n): only submit translations from the develop branch, not feature branches 2026-03-27 15:53:36 -07:00
Gabe Kangas 77c7c08587 fix(api): fix for incorrect error handling of favicon uploads 2026-03-27 15:34:09 -07:00
Owncast d9a424ea79 Bundle embedded web app 2026-03-27 21:46:34 +00:00
826c7e8a9e chore(i18n): update translations from Crowdin (#4829)
Co-authored-by: Owncast <owncast@owncast.online>
2026-03-27 14:25:59 -07:00
Gabe Kangas 3e6f6de032 chore(deps): also ignore dependabot 2026-03-27 14:24:38 -07:00
renovate[bot]andGitHub 3909c506a2 chore(deps): update peter-evans/create-or-update-comment digest to 3e11480 2026-03-27 20:32:16 +00:00
Owncast e9556d44da Bundle embedded web app 2026-03-27 20:30:58 +00:00
Gabe Kangas c85529370b chore(deps): do not run code lint/format on renovate PRs and do not run linter during builds 2026-03-27 13:25:32 -07:00
Owncast 285a722fac Bundle embedded web app 2026-03-27 20:09:55 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
234c696af9 chore(deps): lock file maintenance (#4807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 13:03:35 -07:00
Owncast 177840b94a Bundle embedded web app 2026-03-27 19:01:56 +00:00
renovate[bot]andGitHub 35e684bd10 chore(deps): update dependency cypress to v15.13.0 2026-03-27 15:51:53 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9b4adab8fc chore(deps): bump picomatch in /test/load (#4851)
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:49:41 -07:00
Owncast d74d1b55f0 Bundle embedded web app 2026-03-27 01:08:56 +00:00
renovate[bot]andGitHub 4d52d7d1fa chore(deps): update dependency handlebars to v4.7.9 [security] 2026-03-26 22:44:48 +00:00
Owncast 02559acc39 Bundle embedded web app 2026-03-26 22:43:57 +00:00
renovate[bot]andGitHub 892fd3d89e chore(deps): update typescript-eslint monorepo to v8.57.2 2026-03-26 21:59:57 +00:00
Owncast 0bb372d8cb Bundle embedded web app 2026-03-26 21:58:26 +00:00
renovate[bot]andGitHub ddc8e9bed0 chore(deps): update dependency chromatic to v15.3.1 2026-03-26 17:59:18 +00:00
renovate[bot]andGitHub 5b2a3e3127 chore(deps): update peter-evans/create-or-update-comment digest to df7d5bf 2026-03-26 00:29:37 +00:00
Owncast a476142f23 Bundle embedded web app 2026-03-25 23:03:22 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ad090a0f86 chore(deps): bump socket.io-parser from 4.2.4 to 4.2.6 in /test/load (#4834)
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.4 to 4.2.6.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.4...socket.io-parser@4.2.6)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 15:58:00 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5365cc0325 chore(deps-dev): bump minimatch from 3.1.2 to 3.1.5 in /web (#4812)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 15:57:21 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
37404aac5c chore(deps): bump google.golang.org/grpc from 1.75.0 to 1.79.3 in /tools (#4832)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.75.0 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.75.0...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 15:56:33 -07:00
139 changed files with 2611 additions and 1769 deletions
@@ -1,57 +0,0 @@
name: ActivityPub Federation Tests
on:
push:
paths:
- "activitypub/**"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-federation-tests.yaml"
pull_request:
paths:
- "activitypub/**"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-federation-tests.yaml"
jobs:
federation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install Caddy
run: |
sudo apt-get update
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
- name: Install dependencies
run: |
sudo apt-get install -y snac2 ffmpeg
- name: Run setup
run: |
cd test/automated/activitypub
sudo ./setup.sh
- name: Run federation test
run: |
cd test/automated/activitypub
CI=true USER_COUNT=20 ./run.sh
@@ -1,47 +0,0 @@
name: ActivityPub Follower Validation Tests
on:
push:
paths:
- "activitypub/**"
- "config/**"
- "main.go"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-follower-validation-tests.yaml"
pull_request:
paths:
- "activitypub/**"
- "config/**"
- "main.go"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-follower-validation-tests.yaml"
jobs:
follower-validation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y sqlite3 ffmpeg jq
- name: Run follower validation test
timeout-minutes: 15
run: |
cd test/automated/activitypub
./test-follower-validation.sh
+130
View File
@@ -0,0 +1,130 @@
name: ActivityPub Tests
on:
push:
paths:
- "activitypub/**"
- "core/chat/**"
- "config/**"
- "main.go"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-tests.yaml"
pull_request:
paths:
- "activitypub/**"
- "core/chat/**"
- "config/**"
- "main.go"
- "webserver/**"
- "test/automated/activitypub/**"
- ".github/workflows/activitypub-tests.yaml"
jobs:
federation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install Caddy
run: |
sudo apt-get update
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
- name: Install dependencies
run: |
sudo apt-get install -y snac2 ffmpeg
- name: Run setup
run: |
cd test/automated/activitypub
sudo ./setup.sh
- name: Run federation test
run: |
cd test/automated/activitypub
CI=true USER_COUNT=20 ./test-federation.sh
chat-sanitization-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install Caddy
run: |
sudo apt-get update
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt-get update
sudo apt-get install -y caddy
- name: Install dependencies
run: |
sudo apt-get install -y snac2 ffmpeg
- name: Run setup
run: |
cd test/automated/activitypub
sudo ./setup.sh
- name: Run chat sanitization test
run: |
cd test/automated/activitypub
CI=true ./test-chat-sanitization.sh
follower-validation-test:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: "same_content_newer"
- name: Check out repository code
uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "^1.23"
cache: true
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y sqlite3 ffmpeg jq
- name: Run follower validation test
timeout-minutes: 15
run: |
cd test/automated/activitypub
./test-follower-validation.sh
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
issues: write
steps:
- name: Add comment
uses: peter-evans/create-or-update-comment@9143c495e69aa6ad64fa02b7515ccab6bd35aad1
uses: peter-evans/create-or-update-comment@57232238742e38b2ccc27136ce596ccae7ca28b4
with:
issue-number: ${{ github.event.issue.number }}
body: |
+1 -1
View File
@@ -85,7 +85,7 @@ jobs:
- uses: actions/setup-go@v6
if: steps.changed-files-yaml.outputs.testable_any_changed == 'true'
with:
go-version: '1.26.1'
go-version: '1.26.2'
cache: true
- name: Install Google Chrome
+1 -1
View File
@@ -86,7 +86,7 @@ jobs:
- name: Publish to Chromatic
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true' }}
uses: chromaui/action@v15
uses: chromaui/action@v16
with:
workingDir: web
projectToken: f47410569b62
+1 -1
View File
@@ -59,7 +59,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.26.1'
go-version: '1.26.2'
cache: true
# Initializes the CodeQL tools for scanning.
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version: '1.26.1'
go-version: '1.26.2'
cache: true
- uses: actions/checkout@v6
- name: golangci-lint
+1 -1
View File
@@ -70,7 +70,7 @@ jobs:
- uses: actions/setup-go@v6
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
with:
go-version: '1.26.1'
go-version: '1.26.2'
cache: true
- name: Cache node modules
@@ -38,6 +38,7 @@ on:
jobs:
formatting:
name: Code formatting
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
defaults:
run:
@@ -133,6 +134,7 @@ jobs:
unused-code:
name: Test for unused code
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
defaults:
run:
@@ -197,7 +199,7 @@ jobs:
web-bundle:
name: Build and bundle web project
runs-on: ubuntu-latest
if: github.repository == 'owncast/owncast'
if: github.repository == 'owncast/owncast' && !cancelled()
needs: [formatting, unused-code]
steps:
- id: skip_check
+24
View File
@@ -15,6 +15,7 @@ on:
permissions:
contents: read
pull-requests: write
jobs:
validate-checklist:
@@ -44,6 +45,7 @@ jobs:
}
- name: Validate required checkboxes
id: validate
if: ${{ steps.membership.outputs.result == 'false' }}
uses: actions/github-script@v8
with:
@@ -55,6 +57,7 @@ jobs:
const match = prBody.match(markerPattern);
if (!match) {
core.setOutput('missing_checklist', 'true');
core.setFailed(
'## PR Checklist Validation Failed\n\n' +
'The required checklist section is missing from your PR description.\n\n' +
@@ -86,3 +89,24 @@ jobs:
} else {
console.log('All required checklist items are checked');
}
- name: Comment on PR about missing checklist
if: ${{ always() && steps.validate.outputs.missing_checklist == 'true' }}
uses: actions/github-script@v8
with:
script: |
const body =
'## PR Checklist Missing\n\n' +
'It looks like the **required checklist section** was removed from your PR description. ' +
'This section is needed for the PR checks to pass.\n\n' +
'Please edit your PR description and restore the checklist from ' +
'[the PR template](https://raw.githubusercontent.com/owncast/owncast/refs/heads/develop/.github/PULL_REQUEST_TEMPLATE.MD). ' +
'Once restored, check off each item to confirm you have completed them.\n\n' +
'The checklist section begins with `<!-- REQUIRED-CHECKLIST:START -->` and ends with `<!-- REQUIRED-CHECKLIST:END -->`.';
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: body
});
+2
View File
@@ -16,6 +16,8 @@ on:
# Run the workflow every hour
- cron: '0 * * * *'
push:
branches:
- develop
paths:
- 'web/i18n/en/translation.json'
- 'web/**/*.tsx'
+13 -4
View File
@@ -4,12 +4,24 @@ import (
"fmt"
"github.com/go-fed/activity/streams/vocab"
"github.com/microcosm-cc/bluemonday"
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/persistence/configrepository"
)
// sanitizeActorName strips HTML tags from the ActivityPub actor display name.
// Falls back to the username if the display name is empty or entirely HTML.
func sanitizeActorName(displayName, username string) string {
strict := bluemonday.StrictPolicy()
name := strict.Sanitize(displayName)
if name == "" {
name = strict.Sanitize(username)
}
return name
}
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
configRepository := configrepository.Get()
@@ -30,10 +42,7 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
}
// Send chat message
actorName := actor.Name
if actorName == "" {
actorName = actor.Username
}
actorName := sanitizeActorName(actor.Name, actor.Username)
actorIRI := actor.ActorIriString()
userPrefix := fmt.Sprintf("%s ", actorName)
+120
View File
@@ -0,0 +1,120 @@
package inbox
import (
"testing"
)
func TestSanitizeActorName(t *testing.T) {
tests := []struct {
name string
displayName string
username string
expected string
}{
{
name: "plain display name",
displayName: "Alice",
username: "alice",
expected: "Alice",
},
{
name: "display name with emoji",
displayName: "Alice 🦊",
username: "alice",
expected: "Alice 🦊",
},
{
name: "display name with unicode",
displayName: "Ålice Böb",
username: "alice",
expected: "Ålice Böb",
},
{
name: "empty display name falls back to username",
displayName: "",
username: "alice",
expected: "alice",
},
{
name: "script tag in display name",
displayName: `<script>alert("xss")</script>`,
username: "alice",
expected: "alice",
},
{
name: "iframe injection in display name",
displayName: `<iframe src="https://evil.com" style="position:fixed;top:0;left:0;width:100%;height:100%"></iframe>`,
username: "alice",
expected: "alice",
},
{
name: "img tag in display name",
displayName: `<img src="https://evil.com/track.png">`,
username: "alice",
expected: "alice",
},
{
name: "form injection in display name",
displayName: `<form action="https://evil.com/steal"><input name="pw" type="password"></form>`,
username: "alice",
expected: "alice",
},
{
name: "meta refresh in display name",
displayName: `<meta http-equiv="refresh" content="0;url=https://evil.com">`,
username: "alice",
expected: "alice",
},
{
name: "mixed text and HTML in display name",
displayName: `Alice <script>alert(1)</script> Bob`,
username: "alice",
expected: "Alice Bob",
},
{
name: "custom emoji HTML in display name",
displayName: `Alice :blobcat: <img src="https://instance.com/emoji/blobcat.png" class="custom-emoji">`,
username: "alice",
expected: "Alice :blobcat: ",
},
{
name: "HTML in both display name and username",
displayName: `<script>alert(1)</script>`,
username: `<b>alice</b>`,
expected: "alice",
},
{
name: "entirely HTML display name falls back to username",
displayName: `<div></div>`,
username: "alice",
expected: "alice",
},
{
name: "style tag in display name",
displayName: `<style>body{display:none}</style>Alice`,
username: "alice",
expected: "Alice",
},
{
name: "nested HTML tags",
displayName: `<div><span><a href="https://evil.com">Click me</a></span></div>`,
username: "alice",
expected: "Click me",
},
{
name: "event handler attributes",
displayName: `<img src=x onerror="alert(1)">Alice`,
username: "alice",
expected: "Alice",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizeActorName(tt.displayName, tt.username)
if result != tt.expected {
t.Errorf("sanitizeActorName(%q, %q) = %q, want %q", tt.displayName, tt.username, result, tt.expected)
}
})
}
}
-1
View File
@@ -126,7 +126,6 @@ func SendFediverseAction(eventType string, userAccountName string, image *string
}
message.SetDefaults()
message.RenderBody()
if err := Broadcast(&message); err != nil {
log.Errorln("error sending system message", err)
+7 -7
View File
@@ -16,12 +16,12 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/mattn/go-sqlite3 v1.14.37
github.com/mattn/go-sqlite3 v1.14.42
github.com/microcosm-cc/bluemonday v1.0.27
github.com/mssola/user_agent v0.6.0
github.com/nakabonne/tstorage v0.3.6
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
github.com/oapi-codegen/runtime v1.3.0
github.com/oapi-codegen/runtime v1.4.0
github.com/oschwald/geoip2-golang v1.13.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
@@ -33,9 +33,9 @@ require (
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
github.com/yuin/goldmark v1.7.13
github.com/yuin/goldmark-emoji v1.0.6
golang.org/x/crypto v0.49.0
golang.org/x/mod v0.33.0
golang.org/x/net v0.51.0
golang.org/x/crypto v0.50.0
golang.org/x/mod v0.34.0
golang.org/x/net v0.52.0
golang.org/x/time v0.15.0
gopkg.in/evanphx/json-patch.v5 v5.9.11
mvdan.cc/xurls/v2 v2.6.0
@@ -71,8 +71,8 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+24
View File
@@ -94,6 +94,16 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.39 h1:sIwSjlJGOaRJjw44/HXaeTblZMjseqr6OOio1tz/+JI=
github.com/mattn/go-sqlite3 v1.14.39/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.40 h1:f7+saIsbq4EF86mUqe0uiecQOJYMOdfi5uATADmUG94=
github.com/mattn/go-sqlite3 v1.14.40/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mattn/go-sqlite3 v1.14.41 h1:8p7Pwz5NHkEbWSqc/ygU4CBGubhFFkpgP9KwcdkAHNA=
github.com/mattn/go-sqlite3 v1.14.41/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
@@ -106,6 +116,10 @@ github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2Qd
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA=
github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
github.com/oapi-codegen/runtime v1.3.1 h1:RgDY6J4OGQLbRXhG/Xpt3vSVqYpHQS7hN4m85+5xB9g=
github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
github.com/oapi-codegen/runtime v1.4.0 h1:KLOSFOp7UzkbS7Cs1ms6NBEKYr0WmH2wZG0KKbd2er4=
github.com/oapi-codegen/runtime v1.4.0/go.mod h1:5sw5fxCDmnOzKNYmkVNF8d34kyUeejJEY8HNT2WaPec=
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
@@ -181,6 +195,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -188,6 +204,8 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -201,6 +219,8 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -227,6 +247,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -249,6 +271,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.VideoJS_player__GD36e{width:100%;position:relative;overflow:hidden}@media(hover:hover)and (pointer:fine){.VideoJS_player__GD36e .vjs-control-bar .vjs-control:hover .vjs-control-text{display:block!important;visibility:visible!important;width:auto!important;height:auto!important;clip:auto!important;margin:0!important;position:absolute;bottom:45px;left:50%;transform:translateX(-50%);white-space:nowrap;z-index:1000;padding:5px 10px;background:hsla(0,0%,8%,.9);color:#fff;border-radius:4px;font-size:12px;line-height:1.2;pointer-events:none}.VideoJS_player__GD36e .vjs-control-bar .vjs-control:hover .vjs-control-text:empty,.VideoJS_player__GD36e .vjs-volume-panel:hover .vjs-mute-control:not(:hover) .vjs-control-text{display:none!important}.VideoJS_player__GD36e .vjs-control:first-child:hover .vjs-control-text{left:0;transform:translateX(6px)}.VideoJS_player__GD36e .vjs-control:first-child:hover .vjs-control-text:after{left:15%}.VideoJS_player__GD36e .vjs-control:last-child:hover .vjs-control-text{left:auto;right:0;transform:translateX(-6px)}.VideoJS_player__GD36e .vjs-control:last-child:hover .vjs-control-text:after{left:auto;right:15%;transform:translateX(50%)}}.VideoPoster_poster__6rnLj{display:flex;justify-content:center;width:100%;height:100%}.VideoPoster_image__8kRcw{background-color:#000}.OwncastPlayer_container__CR5Ry{display:grid;width:100%;justify-items:center;height:var(--player-container-height);aspect-ratio:16/9}@media(width <= 1200px){.OwncastPlayer_container__CR5Ry{height:100%;max-height:var(--player-container-height)}}@media only screen and (width <= 768px){.OwncastPlayer_container__CR5Ry{height:var(--player-container-height);max-height:var(--player-container-height)}}.OwncastPlayer_container__CR5Ry .OwncastPlayer_player__dCDjy,.OwncastPlayer_container__CR5Ry .OwncastPlayer_poster__tbpwE{width:100%;grid-column:1;grid-row:1}
-1
View File
@@ -1 +0,0 @@
.VideoJS_player__GD36e{width:100%}.VideoPoster_poster__6rnLj{display:flex;justify-content:center;width:100%;height:100%}.VideoPoster_image__8kRcw{background-color:#000}.OwncastPlayer_container__CR5Ry{display:grid;width:100%;justify-items:center;height:var(--player-container-height);aspect-ratio:16/9}@media(width <= 1200px){.OwncastPlayer_container__CR5Ry{height:100%;max-height:var(--player-container-height)}}@media only screen and (width <= 768px){.OwncastPlayer_container__CR5Ry{height:var(--player-container-height);max-height:var(--player-container-height)}}.OwncastPlayer_container__CR5Ry .OwncastPlayer_player__dCDjy,.OwncastPlayer_container__CR5Ry .OwncastPlayer_poster__tbpwE{width:100%;grid-column:1;grid-row:1}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.12.14'
const PACKAGE_VERSION = '2.13.2'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
certs/
README.md
+36
View File
@@ -0,0 +1,36 @@
FROM golang:1.25-bookworm
RUN apt-get update && apt-get install -y \
gcc make \
curl libcurl4-openssl-dev libssl-dev \
jq sqlite3 ffmpeg \
libnss3-tools \
lsof procps \
&& rm -rf /var/lib/apt/lists/*
# Install mkcert (architecture-aware)
RUN ARCH="$(dpkg --print-architecture)" && \
curl -sL "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-${ARCH}" -o /usr/local/bin/mkcert && \
chmod +x /usr/local/bin/mkcert
# Install Caddy (architecture-aware)
RUN ARCH="$(dpkg --print-architecture)" && \
curl -sL "https://github.com/caddyserver/caddy/releases/download/v2.8.4/caddy_2.8.4_linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin caddy
# Build and install snac2
RUN git clone --depth 1 https://codeberg.org/grunfink/snac2.git /tmp/snac2-src \
&& cd /tmp/snac2-src && make && cp snac /usr/local/bin/snac \
&& rm -rf /tmp/snac2-src
# Install mkcert CA into system trust store so snac2 trusts the test certs
RUN mkcert -install
# Allow git operations on the mounted repo
RUN git config --global --add safe.directory /owncast
WORKDIR /owncast
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+33 -73
View File
@@ -2,83 +2,29 @@
This test verifies Owncast's ActivityPub federation by having snac2 users follow the Owncast instance and confirming message delivery.
## One-Time Setup
All test infrastructure (snac2, Caddy, mkcert, Go) runs inside a Docker container so you don't need to install anything on the host besides Docker.
### 1. Install mkcert
## Prerequisites
- Docker installed and running
## Running the Tests
```bash
# Ubuntu/Debian
sudo apt install mkcert
# Or download directly
curl -sLO https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64
chmod +x mkcert-v1.4.4-linux-amd64
sudo mv mkcert-v1.4.4-linux-amd64 /usr/local/bin/mkcert
```
### 2. Install the local CA
This installs a Certificate Authority into your system's trust store. All certificates generated by mkcert will be trusted.
```bash
mkcert -install
```
### 3. Generate certificates for the test domains
```bash
cd test/automated/activitypub
mkdir -p certs
mkcert -cert-file certs/cert.pem -key-file certs/key.pem owncast.local snac.local localhost 127.0.0.1
```
### 4. Add hosts entries
```bash
sudo sh -c 'echo "127.0.0.1 owncast.local snac.local" >> /etc/hosts'
```
### 5. Install snac2
```bash
# Ubuntu/Debian
sudo apt install snac2
# Or build from source
git clone https://codeberg.org/grunfink/snac2.git
cd snac2
make
sudo make install
```
### 6. Install Caddy
Caddy is used as the HTTPS reverse proxy for TLS termination. It will be installed automatically by `setup.sh`, or you can install it manually:
```bash
# Download binary directly
curl -sL "https://github.com/caddyserver/caddy/releases/download/v2.8.4/caddy_2.8.4_linux_amd64.tar.gz" | sudo tar -xz -C /usr/local/bin caddy
# Or see: https://caddyserver.com/docs/install
```
## Running the Test
```bash
# Run with default 100 users
# Run the federation test with default 100 users
./run.sh
# Run with fewer users for quick testing
USER_COUNT=10 ./run.sh
# Run the follower validation test
./run.sh test-follower-validation.sh
# Keep servers running after test for debugging
KEEP_RUNNING=true ./run.sh
# Adjust follow request throttling (default 0.1s)
FOLLOW_DELAY=0.2 ./run.sh
# Run in CI mode (skip interactive prompts)
CI=true ./run.sh
```
## Configuration Options
@@ -88,7 +34,7 @@ CI=true ./run.sh
| `USER_COUNT` | 100 | Number of test users to create |
| `FOLLOW_DELAY` | 0.1 | Delay in seconds between follow requests |
| `KEEP_RUNNING` | false | Keep servers running after test for debugging |
| `CI` | false | Skip interactive prompts for CI environments |
| `CI` | false | Always true inside the container |
| `PROXY_PORT` | 8443 | HTTPS proxy port |
| `SNAC_PORT` | 9080 | snac2 HTTP port |
| `OWNCAST_PORT` | 8080 | Owncast HTTP port |
@@ -111,23 +57,37 @@ The test reports:
- **Follow Success Rate**: Percentage of follow requests that succeeded
- **Delivery Rate**: Percentage of registered followers who received the message
## Docker Image Details
The Docker image (`owncast-ap-test`) bundles all dependencies:
- Go (for building Owncast)
- snac2 (built from source)
- Caddy (HTTPS reverse proxy)
- mkcert (TLS certificates trusted by the container)
- sqlite3, jq, curl
Go module and build caches are stored in named Docker volumes (`owncast-ap-test-gomod`, `owncast-ap-test-gobuild`) so repeated runs are faster.
## Troubleshooting
### Certificate errors from snac2
### Docker build fails
Make sure you ran `mkcert -install` and generated the certificates. The CA must be in the system trust store for snac2 to trust the certificates.
Make sure Docker is running. On macOS, Docker Desktop or a compatible runtime (colima, OrbStack, etc.) is required.
### Port already in use
Kill any leftover processes:
If a previous container didn't shut down cleanly:
```bash
pkill -f "snac httpd /tmp"
pkill -f "caddy run"
docker ps -a | grep owncast-ap-test
docker rm -f <container_id>
```
### Hosts file not configured
### Cleaning up Docker resources
Verify the entries exist:
```bash
grep -E 'owncast.local|snac.local' /etc/hosts
# Remove the image
docker rmi owncast-ap-test
# Remove Go caches
docker volume rm owncast-ap-test-gomod owncast-ap-test-gobuild
```
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
set -e
# Generate mkcert certificates for the test domains.
# These are placed in a container-local path so they don't leak into the
# mounted source tree.
export CERT_DIR="/tmp/test-certs"
mkdir -p "${CERT_DIR}"
mkcert -cert-file "${CERT_DIR}/cert.pem" \
-key-file "${CERT_DIR}/key.pem" \
owncast.local snac.local localhost 127.0.0.1
# Change CWD away from the mounted repo root so Owncast doesn't pick up the
# host's (possibly wrong-architecture) ffmpeg binary via ./ffmpeg detection.
# Stay inside the git repo so `git rev-parse --show-toplevel` still works.
cd /owncast/test/automated/activitypub
# Run the specified test script (default: test-federation.sh)
TEST_SCRIPT="${1:-test-federation.sh}"
exec "/owncast/test/automated/activitypub/${TEST_SCRIPT}"
+44 -6
View File
@@ -1,14 +1,52 @@
#!/bin/bash
# Run the ActivityPub federation test
# Run the ActivityPub federation test inside a Docker container.
#
# Usage:
# ./run.sh # Run with 100 users
# USER_COUNT=50 ./run.sh # Run with 50 users
# KEEP_RUNNING=true ./run.sh # Keep servers running after test
# ./run.sh # Run federation test with 100 users
# ./run.sh test-follower-validation.sh # Run follower validation test
# USER_COUNT=50 ./run.sh # Run with 50 users
# KEEP_RUNNING=true ./run.sh # Keep servers running after test
#
# Prerequisites:
# Add to /etc/hosts: 127.0.0.1 owncast.local snac.local
# Docker must be installed and running.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${SCRIPT_DIR}/test-federation.sh" "$@"
REPO_ROOT="$(git rev-parse --show-toplevel)"
IMAGE_NAME="owncast-ap-test"
echo "Building Docker image..."
docker build -t "${IMAGE_NAME}" "${SCRIPT_DIR}"
# Collect environment variables to pass through
ENV_ARGS=()
for var in USER_COUNT FOLLOW_DELAY KEEP_RUNNING CI PROXY_PORT SNAC_PORT OWNCAST_PORT CLEAR_SHARED_INBOX_PERCENT; do
if [[ -n "${!var}" ]]; then
ENV_ARGS+=("-e" "${var}=${!var}")
fi
done
# Always skip interactive prompts inside the container
ENV_ARGS+=("-e" "CI=true")
# Port-forward when KEEP_RUNNING is set so the user can access the services
EXTRA_ARGS=()
if [[ "${KEEP_RUNNING}" == "true" ]]; then
OWNCAST_PORT="${OWNCAST_PORT:-8080}"
PROXY_PORT="${PROXY_PORT:-8443}"
EXTRA_ARGS+=("-p" "${OWNCAST_PORT}:${OWNCAST_PORT}" "-p" "${PROXY_PORT}:${PROXY_PORT}")
fi
echo "Running test in Docker container..."
docker run --rm \
--add-host owncast.local:127.0.0.1 \
--add-host snac.local:127.0.0.1 \
-v "${REPO_ROOT}:/owncast" \
-v owncast-ap-test-gomod:/go/pkg/mod \
-v owncast-ap-test-gobuild:/root/.cache/go-build \
"${ENV_ARGS[@]}" \
"${EXTRA_ARGS[@]}" \
"${IMAGE_NAME}" \
"$@"
+520
View File
@@ -0,0 +1,520 @@
#!/bin/bash
# shellcheck disable=SC2317,SC2329 # cleanup() is invoked via trap, not direct call
# shellcheck disable=SC2034 # SNAC_URL is unused but kept for consistency with other AP tests
# Chat Sanitization Test for Fediverse Engagement Events
#
# This test verifies that malicious HTML and markdown in ActivityPub actor
# display names is sanitized before being rendered in chat messages.
#
# The test:
# 1. Starts snac2 + Owncast + HTTPS proxy (same as federation test)
# 2. Creates snac2 users with malicious display names (HTML, markdown)
# 3. Enables engagement display in chat
# 4. Has the malicious users follow Owncast
# 5. Queries the chat messages from the database
# 6. Verifies no HTML tags or markdown artifacts appear in stored messages
#
# Prerequisites:
# Add to /etc/hosts: 127.0.0.1 owncast.local snac.local
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(git rev-parse --show-toplevel)"
# Configuration
FOLLOW_DELAY="${FOLLOW_DELAY:-0.3}"
CI="${CI:-false}"
PROXY_PORT="${PROXY_PORT:-8443}"
SNAC_PORT="${SNAC_PORT:-9080}"
SNAC_HOSTNAME="snac.local"
OWNCAST_PORT="${OWNCAST_PORT:-8080}"
OWNCAST_HOSTNAME="owncast.local"
ADMIN_USER="admin"
ADMIN_PASS="abc123"
FEDERATION_USERNAME="streamer"
# URLs
SNAC_URL="https://${SNAC_HOSTNAME}:${PROXY_PORT}"
OWNCAST_URL="https://${OWNCAST_HOSTNAME}:${PROXY_PORT}"
# Directories
TEMP_DIR=""
SNAC_DATA_DIR=""
SNAC_BIN=""
OWNCAST_DB=""
# PIDs
SNAC_PID=""
OWNCAST_PID=""
PROXY_PID=""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_test() { echo -e "${CYAN}[TEST]${NC} $1"; }
# Test users: username -> malicious display name
# These simulate real-world attack payloads from the security advisory
declare -A MALICIOUS_USERS
MALICIOUS_USERS=(
["htmlscript"]='<script>alert("xss")</script>'
["htmliframe"]='<iframe src="https://evil.com"></iframe>'
["htmlimg"]='<img src="https://evil.com/track.png">'
["htmlform"]='<form action="https://evil.com"><input type="password"></form>'
["htmlmeta"]='<meta http-equiv="refresh" content="0;url=https://evil.com">'
["htmlstyle"]='<style>body{display:none}</style>Visible'
["mdimage"]='![xss](https://evil.com/track.png)'
["mdlink"]='[Click me](https://evil.com)'
["mixedhtml"]='Alice <script>alert(1)</script> Bob'
["cleanuser"]='Legitimate User'
)
# Patterns that must NOT appear in stored chat message bodies
FORBIDDEN_PATTERNS=(
'<script'
'<iframe'
'<img'
'<form'
'<meta'
'<style'
'<input'
'src="https://evil'
'action="https://evil'
'onerror='
'onload='
)
kill_leftover_processes() {
local killed=false
if pkill -f "snac httpd /tmp" 2>/dev/null; then killed=true; fi
if pkill -f "local-proxy.js" 2>/dev/null; then killed=true; fi
local proxy_pid
proxy_pid=$(lsof -ti :"${PROXY_PORT}" 2>/dev/null) || true
if [[ -n "${proxy_pid}" ]]; then
kill "${proxy_pid}" 2>/dev/null || true
killed=true
fi
local snac_pid
snac_pid=$(lsof -ti :"${SNAC_PORT}" 2>/dev/null) || true
if [[ -n "${snac_pid}" ]]; then
kill "${snac_pid}" 2>/dev/null || true
killed=true
fi
if [[ "${killed}" == "true" ]]; then
log_info "Killed leftover processes from previous run"
sleep 1
fi
}
cleanup() {
log_info "Cleaning up..."
if [[ -n "${PROXY_PID}" ]] && kill -0 "${PROXY_PID}" 2>/dev/null; then
kill "${PROXY_PID}" 2>/dev/null || true
wait "${PROXY_PID}" 2>/dev/null || true
fi
if [[ -n "${OWNCAST_PID}" ]] && kill -0 "${OWNCAST_PID}" 2>/dev/null; then
kill "${OWNCAST_PID}" 2>/dev/null || true
wait "${OWNCAST_PID}" 2>/dev/null || true
fi
if [[ -n "${SNAC_PID}" ]] && kill -0 "${SNAC_PID}" 2>/dev/null; then
kill "${SNAC_PID}" 2>/dev/null || true
wait "${SNAC_PID}" 2>/dev/null || true
fi
if [[ -n "${TEMP_DIR}" ]] && [[ -d "${TEMP_DIR}" ]]; then
rm -rf "${TEMP_DIR}"
fi
log_info "Cleanup complete."
}
trap cleanup EXIT
setup_temp_dir() {
TEMP_DIR=$(mktemp -d)
SNAC_DATA_DIR="${TEMP_DIR}/snac-data"
OWNCAST_DB="${TEMP_DIR}/owncast.db"
log_info "Temp directory: ${TEMP_DIR}"
}
check_hosts_entry() {
if ! grep -q "${OWNCAST_HOSTNAME}" /etc/hosts 2>/dev/null || ! grep -q "${SNAC_HOSTNAME}" /etc/hosts 2>/dev/null; then
log_warn "Required /etc/hosts entries not found."
log_warn "Please add: 127.0.0.1 ${OWNCAST_HOSTNAME} ${SNAC_HOSTNAME}"
exit 1
fi
log_info "Hosts entries verified"
}
install_snac2() {
log_info "Setting up snac2..."
if command -v snac &> /dev/null; then
SNAC_BIN=$(command -v snac)
log_info "Using system snac2: ${SNAC_BIN}"
return
fi
local snac_src="${TEMP_DIR}/snac2-src"
log_info "Cloning snac2..."
git clone --depth 1 https://codeberg.org/grunfink/snac2.git "${snac_src}" 2>/dev/null
log_info "Building snac2..."
pushd "${snac_src}" > /dev/null
make
SNAC_BIN="${snac_src}/snac"
popd > /dev/null
log_info "snac2 built: ${SNAC_BIN}"
}
check_certs() {
# CERT_DIR may be set by the Docker entrypoint; fall back to local certs/
CERT_DIR="${CERT_DIR:-${SCRIPT_DIR}/certs}"
if [[ ! -f "${CERT_DIR}/cert.pem" ]] || [[ ! -f "${CERT_DIR}/key.pem" ]]; then
log_error "Certificates not found in ${CERT_DIR}. See README.md for setup."
exit 1
fi
log_info "Using certificates from ${CERT_DIR}"
}
init_snac2() {
log_info "Initializing snac2..."
local snac_host_port="${SNAC_HOSTNAME}:${PROXY_PORT}"
printf "127.0.0.1\n%s\n%s\n\ntest@test.local\n" "${SNAC_PORT}" "${snac_host_port}" | \
"${SNAC_BIN}" init "${SNAC_DATA_DIR}" > /dev/null 2>&1
log_info "snac2 initialized"
}
create_malicious_users() {
log_info "Creating snac2 users with malicious display names..."
local run_id
run_id=$(date +%s%N | sha256sum | head -c 8)
local created=0
for username in "${!MALICIOUS_USERS[@]}"; do
local displayname="${MALICIOUS_USERS[$username]}"
local full_username="${username}${run_id}"
if printf "%s\n%s\n" "${full_username}" "${displayname}" | "${SNAC_BIN}" adduser "${SNAC_DATA_DIR}" > /dev/null 2>&1; then
# Update the key to include the run_id so we can find them later
MALICIOUS_USERS["${username}"]="${displayname}"
# Store the full username for follow requests
SNAC_FULL_USERNAMES["${username}"]="${full_username}"
created=$((created + 1))
log_info " Created user '${full_username}' with display name: ${displayname}"
else
log_error " Failed to create user '${full_username}'"
fi
done
log_info "Created ${created} users with malicious display names"
}
start_proxy() {
log_info "Starting HTTPS reverse proxy (Caddy)..."
if ! command -v caddy &> /dev/null; then
log_error "Caddy is not installed."
return 1
fi
export PROXY_PORT OWNCAST_PORT SNAC_PORT
export CERT_FILE="${CERT_DIR}/cert.pem"
export KEY_FILE="${CERT_DIR}/key.pem"
local caddy_log="${TEMP_DIR}/caddy.log"
caddy run --config "${SCRIPT_DIR}/Caddyfile" --adapter caddyfile > "${caddy_log}" 2>&1 &
PROXY_PID=$!
sleep 2
if ! kill -0 "${PROXY_PID}" 2>/dev/null; then
log_error "Caddy failed to start"
return 1
fi
local max_attempts=10
local attempt=0
while [[ ${attempt} -lt ${max_attempts} ]]; do
if curl -sk "https://127.0.0.1:${PROXY_PORT}/" > /dev/null 2>&1; then
log_info "Caddy proxy is ready"
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
log_error "Caddy proxy did not become ready"
return 1
}
start_snac2() {
log_info "Starting snac2 server..."
local snac_log="${TEMP_DIR}/snac2.log"
DEBUG=0 "${SNAC_BIN}" httpd "${SNAC_DATA_DIR}" > "${snac_log}" 2>&1 &
SNAC_PID=$!
local max_attempts=30
local attempt=0
while [[ ${attempt} -lt ${max_attempts} ]]; do
if curl -s "http://127.0.0.1:${SNAC_PORT}/" > /dev/null 2>&1; then
log_info "snac2 is ready"
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
log_error "snac2 did not become ready"
return 1
}
build_owncast() {
log_info "Building Owncast..."
pushd "${REPO_ROOT}" > /dev/null
CGO_ENABLED=1 go build -o owncast main.go
popd > /dev/null
log_info "Owncast built"
}
start_owncast() {
log_info "Starting Owncast..."
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
OWNCAST_INSECURE_SKIP_VERIFY=true \
"${REPO_ROOT}/owncast" -database "${OWNCAST_DB}" &
OWNCAST_PID=$!
local max_attempts=30
local attempt=0
while [[ ${attempt} -lt ${max_attempts} ]]; do
if curl -s "http://localhost:${OWNCAST_PORT}/api/status" > /dev/null 2>&1; then
log_info "Owncast is ready"
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
log_error "Owncast did not become ready"
return 1
}
configure_owncast() {
log_info "Configuring Owncast..."
local base_url="http://localhost:${OWNCAST_PORT}"
local auth
auth=$(echo -n "${ADMIN_USER}:${ADMIN_PASS}" | base64)
# Set server URL
curl -s -X POST "${base_url}/api/admin/config/serverurl" \
-H "Authorization: Basic ${auth}" \
-H "Content-Type: application/json" \
-d "{\"value\": \"${OWNCAST_URL}\"}" > /dev/null
# Set federation username
curl -s -X POST "${base_url}/api/admin/config/federation/username" \
-H "Authorization: Basic ${auth}" \
-H "Content-Type: application/json" \
-d "{\"value\": \"${FEDERATION_USERNAME}\"}" > /dev/null
# Enable federation
curl -s -X POST "${base_url}/api/admin/config/federation/enable" \
-H "Authorization: Basic ${auth}" \
-H "Content-Type: application/json" \
-d '{"value": true}' > /dev/null
# Disable private mode
curl -s -X POST "${base_url}/api/admin/config/federation/private" \
-H "Authorization: Basic ${auth}" \
-H "Content-Type: application/json" \
-d '{"value": false}' > /dev/null
# Enable engagement display in chat
curl -s -X POST "${base_url}/api/admin/config/federation/showengagement" \
-H "Authorization: Basic ${auth}" \
-H "Content-Type: application/json" \
-d '{"value": true}' > /dev/null
log_info "Owncast configured (engagement display enabled)"
}
send_follow_requests() {
log_info "Sending follow requests from malicious users..."
local owncast_actor="${OWNCAST_URL}/federation/user/${FEDERATION_USERNAME}"
local successful=0
for username in "${!MALICIOUS_USERS[@]}"; do
local full_username="${SNAC_FULL_USERNAMES[$username]}"
local follow_output
follow_output=$("${SNAC_BIN}" follow "${SNAC_DATA_DIR}" "${full_username}" "${owncast_actor}" 2>&1)
local follow_exit=$?
if [[ ${follow_exit} -eq 0 ]] && [[ ! "${follow_output}" =~ "cannot" ]]; then
successful=$((successful + 1))
log_info " ${full_username} followed Owncast"
else
log_warn " ${full_username} follow failed: ${follow_output}"
fi
sleep "${FOLLOW_DELAY}"
done
log_test "${successful}/${#MALICIOUS_USERS[@]} follow requests sent"
# Wait for follow requests to be processed
local user_count=${#MALICIOUS_USERS[@]}
local wait_time=$((15 + user_count))
log_info "Waiting ${wait_time}s for engagement events to be processed..."
sleep "${wait_time}"
}
verify_chat_sanitization() {
log_info "Verifying chat message sanitization..."
local passed=true
local tests_run=0
local tests_passed=0
# Query fediverse engagement messages from the database
local messages
messages=$(sqlite3 "${OWNCAST_DB}" \
"SELECT body FROM messages WHERE eventType IN ('FEDIVERSE_ENGAGEMENT_FOLLOW', 'FEDIVERSE_ENGAGEMENT_LIKE', 'FEDIVERSE_ENGAGEMENT_REPOST');" 2>/dev/null)
if [[ -z "${messages}" ]]; then
log_error "No fediverse engagement messages found in database"
return 1
fi
local message_count
message_count=$(echo "${messages}" | wc -l | tr -d ' ')
log_info "Found ${message_count} engagement messages in chat"
# Test 1: No forbidden HTML patterns in any message
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
tests_run=$((tests_run + 1))
if echo "${messages}" | grep -qi "${pattern}"; then
log_error "FAIL: Found forbidden pattern '${pattern}' in chat messages:"
echo "${messages}" | grep -i "${pattern}" | while read -r line; do
log_error " Body: ${line}"
done
passed=false
else
tests_passed=$((tests_passed + 1))
log_test "PASS: No '${pattern}' found in messages"
fi
done
# Test 2: Verify the clean user's message is present
# Note: snac2 may not serve the display name in actor objects, so we check
# for either the display name or the username fallback.
tests_run=$((tests_run + 1))
local clean_username="${SNAC_FULL_USERNAMES[cleanuser]}"
if echo "${messages}" | grep -q "Legitimate User"; then
tests_passed=$((tests_passed + 1))
log_test "PASS: Clean display name 'Legitimate User' preserved correctly"
elif echo "${messages}" | grep -q "${clean_username}"; then
tests_passed=$((tests_passed + 1))
log_test "PASS: Clean user present via username fallback '${clean_username}'"
else
log_error "FAIL: Clean user message not found in chat"
passed=false
fi
# Test 3: Verify that messages with stripped HTML fell back to expected content
# Messages from users whose display names were entirely HTML should show
# the follow action text but not the HTML
tests_run=$((tests_run + 1))
if echo "${messages}" | grep -q "followed this stream"; then
tests_passed=$((tests_passed + 1))
log_test "PASS: Follow action text present in messages"
else
log_error "FAIL: No 'followed this stream' text found in any message"
passed=false
fi
# Test 4: No markdown image syntax rendered as HTML img tags
tests_run=$((tests_run + 1))
if echo "${messages}" | grep -qi '<img.*src=.*evil'; then
log_error "FAIL: Markdown image syntax was rendered as HTML img tag"
passed=false
else
tests_passed=$((tests_passed + 1))
log_test "PASS: No markdown-rendered img tags found"
fi
# Test 5: No markdown link syntax rendered as HTML anchor tags with evil URLs
tests_run=$((tests_run + 1))
if echo "${messages}" | grep -qi '<a.*href=.*evil'; then
log_error "FAIL: Markdown link syntax was rendered as HTML anchor tag"
passed=false
else
tests_passed=$((tests_passed + 1))
log_test "PASS: No markdown-rendered anchor tags with evil URLs found"
fi
# Print all stored messages for inspection
echo ""
log_info "All stored engagement messages:"
echo "${messages}" | while read -r line; do
log_info " ${line}"
done
echo ""
echo "========================================"
echo "Chat Sanitization Test Results"
echo "========================================"
echo "Tests run: ${tests_run}"
echo "Tests passed: ${tests_passed}"
echo "========================================"
if [[ "${passed}" == "true" ]]; then
echo -e "${GREEN}TEST PASSED${NC}"
return 0
else
echo -e "${RED}TEST FAILED${NC}"
return 1
fi
}
# We need an associative array for the full usernames
declare -A SNAC_FULL_USERNAMES
main() {
kill_leftover_processes
echo ""
echo "========================================"
echo "Chat Sanitization Test"
echo "========================================"
echo ""
# Setup infrastructure
setup_temp_dir
check_hosts_entry
install_snac2
check_certs
init_snac2
create_malicious_users
start_proxy
start_snac2
build_owncast
start_owncast
configure_owncast
sleep 2
# Send follows from malicious users
send_follow_requests
# Verify results
if verify_chat_sanitization; then
exit 0
else
exit 1
fi
}
main "$@"
+51 -13
View File
@@ -47,6 +47,7 @@ OWNCAST_URL="https://${OWNCAST_HOSTNAME}:${PROXY_PORT}"
TEMP_DIR=""
SNAC_DATA_DIR=""
SNAC_BIN=""
OWNCAST_BIN=""
OWNCAST_DB=""
# PIDs and state
@@ -55,6 +56,7 @@ OWNCAST_PID=""
PROXY_PID=""
TEST_STREAM_PID=""
SNAC_USERNAMES=()
CONFIRMED_FOLLOWERS=()
# Colors
RED='\033[0;31m'
@@ -182,8 +184,8 @@ install_snac2() {
}
check_certs() {
# Use pre-generated mkcert certificates from the script directory
CERT_DIR="${SCRIPT_DIR}/certs"
# CERT_DIR may be set by the Docker entrypoint; fall back to local certs/
CERT_DIR="${CERT_DIR:-${SCRIPT_DIR}/certs}"
if [[ ! -f "${CERT_DIR}/cert.pem" ]] || [[ ! -f "${CERT_DIR}/key.pem" ]]; then
log_error "Certificates not found in ${CERT_DIR}"
@@ -390,11 +392,12 @@ verify_snac_shared_inbox() {
build_owncast() {
log_info "Building Owncast..."
OWNCAST_BIN="${TEMP_DIR}/owncast"
pushd "${REPO_ROOT}" > /dev/null
CGO_ENABLED=1 go build -o owncast main.go
CGO_ENABLED=1 go build -o "${OWNCAST_BIN}" main.go
popd > /dev/null
log_info "Owncast built"
log_info "Owncast built: ${OWNCAST_BIN}"
}
start_owncast() {
@@ -403,7 +406,7 @@ start_owncast() {
# Start Owncast with test environment variables and debug flags
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
OWNCAST_INSECURE_SKIP_VERIFY=true \
"${REPO_ROOT}/owncast" -database "${OWNCAST_DB}" &
"${OWNCAST_BIN}" -database "${OWNCAST_DB}" &
OWNCAST_PID=$!
log_info "Owncast started with PID ${OWNCAST_PID}"
@@ -539,8 +542,8 @@ send_follow_requests() {
log_info "snac2 queue has ${queue_count} pending items"
# Give snac2 background thread time to process all pending follows
# Wait longer for more users
local wait_time=$((10 + USER_COUNT / 10))
# Each follow requires an HTTP round-trip; scale wait with user count
local wait_time=$((10 + USER_COUNT / 5))
log_info "Waiting ${wait_time}s for snac2 to process follow requests..."
sleep "${wait_time}"
}
@@ -562,6 +565,28 @@ verify_followers() {
echo "${count}"
}
populate_confirmed_followers() {
# Query Owncast API for actual registered followers and map their IRIs
# back to snac2 usernames so we only check those inboxes for delivery.
CONFIRMED_FOLLOWERS=()
local auth
auth=$(echo -n "${ADMIN_USER}:${ADMIN_PASS}" | base64)
local response
response=$(curl -s "http://localhost:${OWNCAST_PORT}/api/admin/followers?limit=1000" \
-H "Authorization: Basic ${auth}" 2>/dev/null || echo '{"results":[]}')
# Follower IRIs look like https://snac.local:8443/username
while IFS= read -r iri; do
if [[ -n "${iri}" ]]; then
local username="${iri##*/}"
CONFIRMED_FOLLOWERS+=("${username}")
fi
done < <(echo "${response}" | jq -r '.results[]?.link // empty' 2>/dev/null)
log_info "Confirmed ${#CONFIRMED_FOLLOWERS[@]} follower usernames from Owncast API"
}
send_test_message() {
log_info "Sending test message from Owncast..."
@@ -579,9 +604,16 @@ send_test_message() {
}
check_snac_inboxes_count() {
local users_with_messages=0
# Only check users confirmed as followers by Owncast, not all snac2 users.
# snac2's shared inbox distributes messages to all users it considers followers,
# which may include users whose follow hasn't been registered by Owncast yet.
local usernames_to_check=("${CONFIRMED_FOLLOWERS[@]}")
if [[ ${#usernames_to_check[@]} -eq 0 ]]; then
usernames_to_check=("${SNAC_USERNAMES[@]}")
fi
for username in "${SNAC_USERNAMES[@]}"; do
local users_with_messages=0
for username in "${usernames_to_check[@]}"; do
if user_has_message "${username}"; then
users_with_messages=$((users_with_messages + 1))
fi
@@ -614,11 +646,16 @@ user_has_message() {
check_snac_inboxes() {
log_info "Checking snac2 user inboxes for delivered messages..." >&2
local usernames_to_check=("${CONFIRMED_FOLLOWERS[@]}")
if [[ ${#usernames_to_check[@]} -eq 0 ]]; then
usernames_to_check=("${SNAC_USERNAMES[@]}")
fi
local users_with_messages=0
local users_without_messages=()
local total=${#SNAC_USERNAMES[@]}
local total=${#usernames_to_check[@]}
for username in "${SNAC_USERNAMES[@]}"; do
for username in "${usernames_to_check[@]}"; do
if user_has_message "${username}"; then
users_with_messages=$((users_with_messages + 1))
else
@@ -626,7 +663,7 @@ check_snac_inboxes() {
fi
done
log_test "${users_with_messages}/${total} users received the message" >&2
log_test "${users_with_messages}/${total} confirmed followers received the message" >&2
if [[ ${#users_without_messages[@]} -gt 0 ]] && [[ ${#users_without_messages[@]} -le 10 ]]; then
log_warn "Users missing messages: ${users_without_messages[*]}" >&2
@@ -847,7 +884,7 @@ main() {
# Wait for all followers to be registered (with timeout)
local followers=0
local max_wait=60 # Maximum seconds to wait for followers
local max_wait=$((60 + USER_COUNT)) # Scale timeout with user count
local waited=0
local check_interval=2
@@ -889,6 +926,7 @@ main() {
echo "----------------------------------------"
echo "STEP 4: Owncast sends message to followers"
echo "----------------------------------------"
populate_confirmed_followers
send_test_message
echo ""
@@ -31,6 +31,7 @@ TEST_START_TIME=""
# Directories
TEMP_DIR=""
OWNCAST_BIN=""
OWNCAST_DB=""
# PIDs
@@ -151,11 +152,12 @@ setup_temp_dir() {
build_owncast() {
log_info "Building Owncast..."
OWNCAST_BIN="${TEMP_DIR}/owncast"
pushd "${REPO_ROOT}" > /dev/null
CGO_ENABLED=1 go build -o owncast main.go
CGO_ENABLED=1 go build -o "${OWNCAST_BIN}" main.go
popd > /dev/null
log_info "Owncast built"
log_info "Owncast built: ${OWNCAST_BIN}"
}
start_owncast() {
@@ -164,7 +166,7 @@ start_owncast() {
# Start Owncast with test configuration
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
OWNCAST_INSECURE_SKIP_VERIFY=true \
"${REPO_ROOT}/owncast" \
"${OWNCAST_BIN}" \
-database "${OWNCAST_DB}" \
-followervalidationinterval "${VALIDATION_INTERVAL}" \
-enableVerboseLogging &

Some files were not shown because too many files have changed in this diff Show More