Merge remote-tracking branch 'origin/develop' into shigusegubu-new

This commit is contained in:
Henry Jameson 2026-05-11 16:31:28 +03:00
commit 6ff1d10e22
98 changed files with 3345 additions and 331 deletions

3
.gitignore vendored
View file

@ -56,6 +56,9 @@ pleroma.iml
# asdf # asdf
.tool-versions .tool-versions
# mise
mise.toml
# Editor temp files # Editor temp files
*~ *~
*# *#

View file

@ -1,9 +1,19 @@
when: when:
- event: pull_request - event: pull_request
labels:
platform: linux/amd64
variables:
script_file_entrypoint: &script_file_entrypoint
- /bin/sh
- -c
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
steps: steps:
check-changelog: check-changelog:
image: docker.io/alpine:3.23 image: docker.io/alpine:3.23
entrypoint: *script_file_entrypoint
commands: commands:
- apk add --no-cache git - apk add --no-cache git
- sh ./tools/check-changelog - sh ./tools/check-changelog

View file

@ -0,0 +1,60 @@
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ]
- event: tag
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: stable
depends_on:
- docker
skip_clone: true
labels:
platform: linux/amd64
steps:
docker-develop-combine:
image: git.fluffytail.org/phnt/wpc-docker-tagger:latest
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
settings: &docker_settings
registry: "git.pleroma.social"
image: "pleroma/pleroma"
architectures: [amd64, arm64]
tags:
- latest
- develop
- ${CI_COMMIT_SHA:0:8}
username:
from_secret: pleroma-ci-user
password:
from_secret: pleroma-ci-password
docker-stable-combine:
image: git.fluffytail.org/phnt/wpc-docker-tagger:latest
when:
- evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""'
settings:
<<: *docker_settings
tags: &stable_docker_tags
- latest
- stable
- ${CI_COMMIT_SHA:0:8}
docker-stable-tag-combine:
image: git.fluffytail.org/phnt/wpc-docker-tagger:latest
when:
- event: tag
- evaluate: 'CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""'
settings:
<<: *docker_settings
tags:
- <<: *stable_docker_tags
- ${CI_COMMIT_TAG}

96
.woodpecker/docker.yaml Normal file
View file

@ -0,0 +1,96 @@
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**", "Dockerfile" ]
- event: tag
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: stable
matrix:
platform:
- linux/amd64
- linux/arm64
# This is needed for the when clauses below.
labels:
platform: ${platform}
memory: 'high'
variables:
docker_variables: &docker_variables
repo: pleroma/pleroma
registry: git.pleroma.social
username:
from_secret: pleroma-ci-user
password:
from_secret: pleroma-ci-password
kaniko_image: &kaniko_image woodpeckerci/plugin-kaniko:2.3.1
steps:
docker-develop-amd64:
image: woodpeckerci/plugin-kaniko:2.3.1
when:
- evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
settings:
<<: *docker_variables
tags:
- latest-amd64
- develop-amd64
- ${CI_COMMIT_SHA:0:8}-amd64
docker-develop-arm64:
image: woodpeckerci/plugin-kaniko:2.3.1
when:
- evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
settings:
<<: *docker_variables
tags:
- latest-arm64
- develop-arm64
- ${CI_COMMIT_SHA:0:8}-arm64
docker-stable-amd64:
image: *kaniko_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""'
settings:
<<: *docker_variables
tags: &amd64_tags
- latest-amd64
- stable-amd64
- ${CI_COMMIT_SHA:0:8}-amd64
docker-stable-tag-amd64:
image: *kaniko_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""'
settings:
<<: *docker_variables
tags:
- <<: *amd64_tags
- ${CI_COMMIT_TAG}-amd64
docker-stable-arm64:
image: *kaniko_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG == ""'
settings:
<<: *docker_variables
tags: &arm64_tags
- latest-arm64
- stable-arm64
- ${CI_COMMIT_SHA:0:8}-arm64
docker-stable-tag-arm64:
image: *kaniko_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable" && CI_COMMIT_TAG != ""'
settings:
<<: *docker_variables
tags:
- <<: *arm64_tags
- ${CI_COMMIT_TAG}-arm64

View file

@ -1,14 +1,24 @@
when: when:
- event: pull_request - event: pull_request
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
- event: push - event: push
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
labels:
platform: linux/amd64
variables:
script_file_entrypoint: &script_file_entrypoint
- /bin/sh
- -c
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
steps: steps:
mix-format: mix-format:
image: &elixir-image image: &elixir-image
docker.io/elixir:1.15-alpine docker.io/elixir:1.15-alpine
entrypoint: *script_file_entrypoint
failure: ignore failure: ignore
commands: commands:
- | - |
@ -19,6 +29,7 @@ steps:
credo: credo:
image: *elixir-image image: *elixir-image
entrypoint: *script_file_entrypoint
failure: ignore failure: ignore
environment: environment:
MIX_ENV: test MIX_ENV: test
@ -55,6 +66,7 @@ steps:
ensure-status: ensure-status:
image: *elixir-image image: *elixir-image
entrypoint: *script_file_entrypoint
commands: | commands: |
if test -f fail.stamp; then if test -f fail.stamp; then
echo "One or more previous steps fails. Failing workflow..." echo "One or more previous steps fails. Failing workflow..."

292
.woodpecker/otp-musl.yaml Normal file
View file

@ -0,0 +1,292 @@
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
- event: tag
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: stable
matrix:
platform:
- linux/amd64
- linux/arm
- linux/arm64
# This is needed for the when clauses below.
# When the platform clause is fixed, this might not be needed anymore
labels:
platform: ${platform}
variables:
script_file_entrypoint: &script_file_entrypoint
- /bin/sh
- -c
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
build_cmds: &build_cmds
- apk add git build-base cmake file-dev openssl vips-dev zip
- echo "import Config" > config/prod.secret.exs
- mix local.hex --force
- mix local.rebar --force
- mix deps.get --only prod
- mkdir release
- export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH}
- mix release --path release
build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-alpine-3.22.1
build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3-alpine
build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-alpine-3.22.1
artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
artifacts_uploader_settings: &artifacts_uploader_settings
user:
from_secret: pleroma-ci-user
password:
from_secret: pleroma-ci-password
owner: 'pleroma'
env: &env
MIX_ENV: prod
VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS
steps:
otp-develop-amd64-musl:
image: *build_image_amd64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
environment: *env
commands: &amd64_build
- <<: *build_cmds
- zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release
otp-stable-amd64-musl:
image: *build_image_amd64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
environment: *env
commands: *amd64_build
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
otp-stable-tag-amd64-musl:
image: *build_image_amd64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
environment: *env
commands:
- <<: *build_cmds
- zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip release
otp-develop-arm-musl:
image: *build_image_arm
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
environment: *env
commands: &arm_build
- <<: *build_cmds
- zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip release
otp-stable-arm-musl:
image: *build_image_arm
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
environment: *env
commands: *arm_build
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
otp-stable-tag-arm-musl:
image: *build_image_arm
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
environment: *env
commands:
- <<: *build_cmds
- zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip release
otp-develop-arm64-musl:
image: *build_image_arm64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
environment: *env
commands: &arm64_build
- <<: *build_cmds
- zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release
otp-stable-arm64-musl:
image: *build_image_arm64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
environment: *env
commands: *arm64_build
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
otp-stable-tag-arm64-musl:
image: *build_image_arm64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
environment: *env
commands:
- <<: *build_cmds
- zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip release
upload-artifacts-amd64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl
package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
update: 'true'
upload-latest-amd64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64-musl
package_version: latest
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
file_name: pleroma.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-artifacts-tag-amd64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-amd64-musl
package_version: stable-${CI_COMMIT_SHA:0:8}-amd64-musl
file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-latest-tag-amd64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-amd64-musl
package_version: latest
file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64-musl.zip
file_name: pleroma.zip
update: 'true'
upload-artifacts-arm-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl
package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip
file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip
update: 'true'
upload-latest-arm-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm-musl
package_version: latest
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm-musl.zip
file_name: pleroma.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-artifacts-tag-arm-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm-musl
package_version: stable-${CI_COMMIT_SHA:0:8}-arm-musl
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip
file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-latest-tag-arm-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm-musl
package_version: latest
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm-musl.zip
file_name: pleroma.zip
update: 'true'
upload-artifacts-arm64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl
package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
update: 'true'
upload-latest-arm64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64-musl
package_version: latest
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
file_name: pleroma.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-artifacts-tag-arm64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm64-musl
package_version: stable-${CI_COMMIT_SHA:0:8}-arm64-musl
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-latest-tag-arm64-musl:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm64-musl
package_version: latest
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64-musl.zip
file_name: pleroma.zip
update: 'true'

293
.woodpecker/otp.yaml Normal file
View file

@ -0,0 +1,293 @@
when:
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
- event: tag
- event: manual
branch: ${CI_REPO_DEFAULT_BRANCH}
- event: manual
branch: stable
matrix:
platform:
- linux/amd64
- linux/arm
- linux/arm64
# This is needed for the when clauses below.
# When the platform clause is fixed, this might not be needed anymore
labels:
platform: ${platform}
variables:
script_file_entrypoint: &script_file_entrypoint
- /bin/sh
- -c
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
build_cmds: &build_cmds
- apt-get update && apt-get install -y cmake libmagic-dev libvips-dev erlang-dev git build-essential zip
- echo "import Config" > config/prod.secret.exs
- mix local.hex --force
- mix local.rebar --force
- mix deps.get --only prod
- mkdir release
- export PLEROMA_BUILD_BRANCH=${CI_COMMIT_BRANCH}
- mix release --path release
build_image_amd64: &build_image_amd64 docker.io/hexpm/elixir-amd64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716
build_image_arm: &build_image_arm docker.io/arm32v7/elixir:1.17.3
build_image_arm64: &build_image_arm64 docker.io/hexpm/elixir-arm64:1.17.3-erlang-27.3.4.2-ubuntu-noble-20250716
artifacts_uploader_image: &artifacts_uploader_image docker.io/woodpeckercommunity/plugin-gitea-package:0.5.0
artifacts_uploader_settings: &artifacts_uploader_settings
user:
from_secret: pleroma-ci-user
password:
from_secret: pleroma-ci-password
owner: 'pleroma'
env: &env
MIX_ENV: prod
VIX_COMPILATION_MODE: PLATFORM_PROVIDED_LIBVIPS
DEBIAN_FRONTEND: noninteractive
steps:
otp-develop-amd64:
image: *build_image_amd64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/amd64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
environment: *env
commands: &amd64_build
- <<: *build_cmds
- zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip release
otp-stable-amd64:
image: *build_image_amd64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
environment: *env
commands: *amd64_build
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
otp-stable-tag-amd64:
image: *build_image_amd64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
environment: *env
commands:
- <<: *build_cmds
- zip -9rq stable-${CI_COMMIT_SHA:0:8}-amd64.zip release
otp-develop-arm:
image: *build_image_arm
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
environment: *env
commands: &arm_build
- <<: *build_cmds
- zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip release
otp-stable-arm:
image: *build_image_arm
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
environment: *env
commands: *arm_build
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
otp-stable-tag-arm:
image: *build_image_arm
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
environment: *env
commands:
- <<: *build_cmds
- zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm.zip release
otp-develop-arm64:
image: *build_image_arm64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm64" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
environment: *env
commands: &arm64_build
- <<: *build_cmds
- zip -9rq ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip release
otp-stable-arm64:
image: *build_image_arm64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual" && CI_COMMIT_BRANCH == "stable"'
environment: *env
commands: *arm64_build
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
otp-stable-tag-arm64:
image: *build_image_arm64
entrypoint: *script_file_entrypoint
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
environment: *env
commands:
- <<: *build_cmds
- zip -9rq stable-${CI_COMMIT_SHA:0:8}-arm64.zip release
upload-artifacts-amd64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64
package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip
file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip
update: 'true'
upload-latest-amd64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-amd64
package_version: latest
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-amd64.zip
file_name: pleroma.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-artifacts-tag-amd64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-amd64
package_version: stable-${CI_COMMIT_SHA:0:8}-amd64
file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip
file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-amd64.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-latest-tag-amd64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/amd64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-amd64
package_version: latest
file_source: ./stable-${CI_COMMIT_SHA:0:8}-amd64.zip
file_name: pleroma.zip
update: 'true'
upload-artifacts-arm:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm
package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip
file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip
update: 'true'
upload-latest-arm:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm
package_version: latest
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm.zip
file_name: pleroma.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-artifacts-tag-arm:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm
package_version: stable-${CI_COMMIT_SHA:0:8}-arm
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip
file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-latest-tag-arm:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm
package_version: latest
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm.zip
file_name: pleroma.zip
update: 'true'
upload-artifacts-arm64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64
package_version: ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip
file_name: pleroma-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip
update: 'true'
upload-latest-arm64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "push" && CI_COMMIT_BRANCH == "${CI_REPO_DEFAULT_BRANCH}"'
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "manual"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-${CI_COMMIT_BRANCH}-arm64
package_version: latest
file_source: ./${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:8}-arm64.zip
file_name: pleroma.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-artifacts-tag-arm64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm64
package_version: stable-${CI_COMMIT_SHA:0:8}-arm64
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip
file_name: pleroma-stable-${CI_COMMIT_SHA:0:8}-arm64.zip
update: 'true'
# Tag events don't have CI_COMMIT_BRANCH set, hardcode stable
upload-latest-tag-arm64:
image: *artifacts_uploader_image
when:
- evaluate: 'platform == "linux/arm64" && CI_PIPELINE_EVENT == "tag"'
settings:
<<: *artifacts_uploader_settings
package_name: pleroma-otp-stable-arm64
package_version: latest
file_source: ./stable-${CI_COMMIT_SHA:0:8}-arm64.zip
file_name: pleroma.zip
update: 'true'

View file

@ -1,16 +1,26 @@
when: when:
- event: pull_request - event: pull_request
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
- event: push - event: push
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
depends_on: depends_on:
- lint - lint
labels:
platform: linux/amd64
variables:
script_file_entrypoint: &script_file_entrypoint
- /bin/sh
- -c
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
steps: steps:
unit-testing-elixir-1.15: unit-testing-elixir-1.15:
image: elixir:1.15-alpine image: elixir:1.15-alpine
entrypoint: *script_file_entrypoint
environment: environment:
MIX_ENV: test MIX_ENV: test
DB_HOST: postgres DB_HOST: postgres

View file

@ -1,16 +1,26 @@
when: when:
- event: pull_request - event: pull_request
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
- event: push - event: push
branch: ${CI_REPO_DEFAULT_BRANCH} branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ] path: [ "**/*.ex", "**/*.eex", "**/*.exs", "mix.lock", ".woodpecker/**" ]
depends_on: depends_on:
- lint - lint
labels:
platform: linux/amd64
variables:
script_file_entrypoint: &script_file_entrypoint
- /bin/sh
- -c
- 'printf "%s" "$CI_SCRIPT" | base64 -d > /tmp/ci-script.sh && /bin/sh -xe /tmp/ci-script.sh'
steps: steps:
unit-testing-elixir-1.18: unit-testing-elixir-1.18:
image: elixir:1.18-otp-27-alpine image: elixir:1.18-otp-27-alpine
entrypoint: *script_file_entrypoint
environment: environment:
MIX_ENV: test MIX_ENV: test
DB_HOST: postgres DB_HOST: postgres

View file

@ -4,6 +4,60 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.10.2
### Security
- ActivityPub: Fixed failed-signature inbox retry handling and signer identity checks to prevent spoofed remote activities from being processed
## 2.10.1
### Changed
- Move avatar_description and header_description fields to the account object
- Update Bandit to 1.10.4
- No-op code correctness improvements detected by Elixir 1.19 compiler
- Downgrade Hackney to 1.20.1
- Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney
- Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation
- Paginate follow requests
- Moved Phoenix LiveDashboard to /pleroma/live_dashboard
- Add mute/block expiry to the relationship object
- Filter indexable activities before inserting indexing jobs into the queue.
### Added
- Allow assigning users to reports
- Allow fine-grained announce visibilities
- Add immutable tag on cache-control header for several endpoints that's serving the same exact things.
- Add reasonable defaults for :database_config_whitelist
- Support lists `exclusive` param
- Add v1/instance/domain_blocks endpoint
- Add /api/v2/instance profile fields limits info used by Mastodon
- Added Oban Web dashboard located at /pleroma/oban
- Add instructions on how to run a release in docker, to make it easier to run on older distros.
### Fixed
- Fix the daily email digest job which was not executing
- Encode custom emoji URLs in EmojiReact activity tags.
- Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled
- Fix fetching Hubzilla Actors with alsoKnownAs as string
- Fix /phoenix/live_dashboard redirect not working when user added a path segment
- Fix 404 error codes for missing static files
- Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input.
- Correct old migrations for expiring activities and user access tokens.
- Federate `votersCount` correctly
- DB prune: Check if user follows hashtag with no objects before deletion
- Stop the rate limiter from crashing when run with wrong settings.
- Restore embeds route
- ReverseProxy: Recursively follow redirects until redirect_limit is reached
- Fix compilation with vips-8.18.0 with bumping to vix 0.36.0
### Removed
- Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS)
## 2.10 ## 2.10
### Security ### Security

View file

@ -1 +0,0 @@
Allow assigning users to reports

View file

@ -1 +0,0 @@
Move avatar_description and header_description fields to the account object

View file

@ -1 +0,0 @@
Update Bandit to 1.10.4

View file

@ -1 +0,0 @@
Various bookmark folders-related improvements

View file

@ -1 +0,0 @@
Allow fine-grained announce visibilities

View file

@ -1 +0,0 @@
Add immutable tag on cache-control header for several endpoints that's serving the same exact things.

View file

@ -0,0 +1 @@
litepub-0.1.jsonld cleanup

View file

@ -1 +0,0 @@
Add reasonable defaults for :database_config_whitelist

View file

@ -1 +0,0 @@
No-op code correctness improvements detected by Elixir 1.19 compiler

View file

@ -1 +0,0 @@
Fix the daily email digest job which was not executing

View file

@ -1 +0,0 @@
Encode custom emoji URLs in EmojiReact activity tags.

View file

@ -1 +0,0 @@
Support lists `exclusive` param

View file

@ -1 +0,0 @@
Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled

View file

@ -1 +0,0 @@
Downgrade Hackney to 1.20.1

View file

@ -1 +0,0 @@
Use a custom redirect handler to ensure MediaProxy redirects are followed with Hackney

View file

@ -1 +0,0 @@
Update Hackney, the default HTTP client, to the latest release which supports Happy Eyeballs for improved IPv6 federation

View file

@ -1 +0,0 @@
Fix fetching Hubzilla Actors with alsoKnownAs as string

View file

@ -1 +0,0 @@
Docs: Removed outdated, incorrect, unmaintained and inappropriate installation documentation (Arch, NetBSD, NixOS)

View file

@ -1 +0,0 @@
Add v1/instance/domain_blocks endpoint

View file

@ -1 +0,0 @@
Add /api/v2/instance profile fields limits info used by Mastodon

View file

@ -1 +0,0 @@
Fix /phoenix/live_dashboard redirect not working when user added a path segment

View file

@ -0,0 +1 @@
Add backend support for Misskey Markdown (MFM) posts

View file

@ -1 +0,0 @@
Fix 404 error codes for missing static files

View file

@ -1 +0,0 @@
Fix OAuth app registration to accept `redirect_uris` as an array of strings (RFC 7591), while keeping backwards compatibility with string input.

View file

@ -1 +0,0 @@
Added Oban Web dashboard located at /pleroma/oban

View file

@ -1 +0,0 @@
Correct old migrations for expiring activities and user access tokens.

View file

@ -1 +0,0 @@
Paginate follow requests

View file

@ -1 +0,0 @@
Moved Phoenix LiveDashboard to /pleroma/live_dashboard

View file

@ -0,0 +1 @@
Updated Pleroma-FE build URL after Forgejo migration

View file

@ -0,0 +1 @@
Fix votersCount inflation when same voter picks multiple options

View file

@ -1 +0,0 @@
Federate `votersCount` correctly

View file

@ -1 +0,0 @@
DB prune: Check if user follows hashtag with no objects before deletion

View file

@ -1 +0,0 @@
Stop the rate limiter from crashing when run with wrong settings.

View file

@ -1 +0,0 @@
Reduce the number of flaky tests by making them sync if they affect the global state, and silence noisy test output.

View file

@ -1 +0,0 @@
Add mute/block expiry to the relationship object

View file

@ -1 +0,0 @@
Add instructions on how to run a release in docker, to make it easier to run on older distros.

View file

@ -1 +0,0 @@
Restore embeds route

View file

@ -1 +0,0 @@
ReverseProxy: Recursively follow redirects until redirect_limit is reached

View file

@ -1 +0,0 @@
Filter indexable activities before inserting indexing jobs into the queue.

View file

@ -1 +0,0 @@
Update comment for prepare_object, rename prepare_outgoing

View file

@ -1 +0,0 @@
Avoid code duplication in UserView

View file

@ -1 +0,0 @@
Fix compilation with vips-8.18.0 with bumping to vix 0.36.0

View file

@ -203,7 +203,8 @@ config :pleroma, :instance,
"text/plain", "text/plain",
"text/html", "text/html",
"text/markdown", "text/markdown",
"text/bbcode" "text/bbcode",
"text/x.misskeymarkdown"
], ],
autofollowed_nicknames: [], autofollowed_nicknames: [],
autofollowing_nicknames: [], autofollowing_nicknames: [],
@ -775,7 +776,7 @@ config :pleroma, :frontends,
"name" => "pleroma-fe", "name" => "pleroma-fe",
"git" => "https://git.pleroma.social/pleroma/pleroma-fe", "git" => "https://git.pleroma.social/pleroma/pleroma-fe",
"build_url" => "build_url" =>
"https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build", "https://git.pleroma.social/api/packages/pleroma/generic/pleroma-fe-builds/${ref}/latest.zip",
"ref" => "develop" "ref" => "develop"
}, },
"fedi-fe" => %{ "fedi-fe" => %{

View file

@ -815,7 +815,8 @@ config :pleroma, :config_description, [
"text/plain", "text/plain",
"text/html", "text/html",
"text/markdown", "text/markdown",
"text/bbcode" "text/bbcode",
"text/x.misskeymarkdown"
] ]
}, },
%{ %{
@ -1394,7 +1395,13 @@ config :pleroma, :config_description, [
label: "Post Content Type", label: "Post Content Type",
type: {:dropdown, :atom}, type: {:dropdown, :atom},
description: "Default post formatting option", description: "Default post formatting option",
suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"] suggestions: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
]
}, },
%{ %{
key: :redirectRootNoLogin, key: :redirectRootNoLogin,

View file

@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
end end
def markdown_to_html(text, opts) do
Earmark.as_html!(
text,
%Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts)
)
end
def html_escape({text, mentions, hashtags}, type) do def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags} {html_escape(text, type), mentions, hashtags}
end end
@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do
HTML.filter_tags(text) HTML.filter_tags(text)
end end
def html_escape(text, "text/x.misskeymarkdown") do
HTML.filter_tags(text)
end
def html_escape(text, "text/plain") do def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true) Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk -> |> Enum.map_every(2, fn chunk ->

View file

@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do
end end
defp download_build(frontend_info, dest) do defp download_build(frontend_info, dest) do
Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}")
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"]) url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}")
with {:ok, %{status: 200, body: zip_body}} <- with {:ok, %{status: 200, body: zip_body}} <-
Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do

View file

@ -372,13 +372,21 @@ defmodule Pleroma.Object do
option option
end) end)
voters = [actor | object.data["voters"] || []] |> Enum.uniq() existing_voters = object.data["voters"] || []
voters = [actor | existing_voters] |> Enum.uniq()
new_voter? = actor not in existing_voters
existing_voters_count = object.data["votersCount"]
voters_count = voters_count =
if Map.has_key?(object.data, "votersCount") do cond do
object.data["votersCount"] + 1 is_integer(existing_voters_count) and new_voter? ->
else existing_voters_count + 1
length(voters)
is_integer(existing_voters_count) ->
existing_voters_count
true ->
length(voters)
end end
data = data =

View file

@ -5,7 +5,10 @@
defmodule Pleroma.ReleaseTasks do defmodule Pleroma.ReleaseTasks do
@repo Pleroma.Repo @repo Pleroma.Repo
def run(args) do # TODO: Kept for some backwards compatibility with buggy pleroma_ctl,
# if a mismatch between pleroma_ctl and Pleroma accidentaly happens.
# Remove in the future.
def run(args) when is_binary(args) do
[task | args] = String.split(args) [task | args] = String.split(args)
case task do case task do
@ -16,6 +19,20 @@ defmodule Pleroma.ReleaseTasks do
end end
end end
# HACK: Script arguments need to be received as a list, otherwise (quoted) arguments with
# whitespace will be broken. Previously the broken string form above was used,
# escaping in the shell does not help.
def run(args) when is_list(args) do
[task | args] = args
case task do
"migrate" -> migrate(args)
"create" -> create()
"rollback" -> rollback(args)
task -> mix_task(task, args)
end
end
def find_module(task) do def find_module(task) do
module_name = module_name =
task task

View file

@ -1677,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
show_birthday = !!birthday show_birthday = !!birthday
# if WebFinger request was already done, we probably have acct, otherwise with {:ok, nickname} <- nickname_from_actor(data, additional) do
# we request WebFinger here {:ok,
nickname = additional[:nickname_from_acct] || generate_nickname(data) %{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
banner: normalize_image(data["image"]),
fields: fields,
emoji: emojis,
is_locked: is_locked,
is_discoverable: is_discoverable,
invisible: invisible,
avatar: normalize_image(data["icon"]),
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages,
birthday: birthday,
show_birthday: show_birthday,
pinned_objects: pinned_objects,
nickname: nickname
}}
end
end
%{ defp nickname_from_actor(data, additional) do
ap_id: data["id"], generated = generated_nickname(data)
uri: get_actor_url(data["url"]),
banner: normalize_image(data["image"]), case additional[:nickname_from_acct] do
fields: fields, ^generated when is_binary(generated) ->
emoji: emojis, {:ok, generated}
is_locked: is_locked,
is_discoverable: is_discoverable, acct when is_binary(acct) ->
invisible: invisible, with ^acct <- webfinger_nickname(data) do
avatar: normalize_image(data["icon"]), {:ok, acct}
name: data["name"], else
follower_address: data["followers"], _ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}}
following_address: data["following"], end
featured_address: featured_address,
bio: data["summary"] || "", _ ->
actor_type: actor_type, {:ok, generate_nickname(data)}
also_known_as: normalize_also_known_as(data["alsoKnownAs"]), end
public_key: public_key, end
inbox: data["inbox"],
shared_inbox: shared_inbox, defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id})
accepts_chat_messages: accepts_chat_messages, when is_binary(username) and is_binary(ap_id) do
birthday: birthday, case URI.parse(ap_id) do
show_birthday: show_birthday, %URI{host: host} when is_binary(host) -> "#{username}@#{host}"
pinned_objects: pinned_objects, _ -> nil
nickname: nickname end
} end
defp generated_nickname(_), do: nil
defp webfinger_nickname(data) do
with generated when is_binary(generated) <- generated_nickname(data),
{:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated),
true <- ap_id == data["id"] do
acct
end
end end
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
generated = "#{username}@#{URI.parse(data["id"]).host}" generated = generated_nickname(data)
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
case WebFinger.finger(generated) do case webfinger_nickname(data) do
{:ok, %{"subject" => "acct:" <> acct}} -> acct acct when is_binary(acct) -> acct
_ -> generated _ -> generated
end end
else else
@ -1794,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp collection_private(_data), do: {:ok, true} defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data, additional \\ []) do def user_data_from_user_object(data, additional \\ []) do
with {:ok, data} <- MRF.filter(data) do with {:ok, data} <- MRF.filter(data),
{:ok, object_to_user_data(data, additional)} {:ok, data} <- object_to_user_data(data, additional) do
{:ok, data}
else else
{:error, _} = e -> e
e -> {:error, e} e -> {:error, e}
end end
end end

View file

@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end end
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
Federator.incoming_ap_doc(%{ Federator.incoming_failed_signature_ap_doc(%{
method: conn.method, method: conn.method,
req_headers: conn.req_headers, req_headers: conn.req_headers,
request_path: conn.request_path, request_path: conn.request_path,

View file

@ -6,9 +6,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Changeset import Ecto.Changeset
@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
end end
field(:replies, {:array, ObjectValidators.ObjectID}, default: []) field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
field(:source, :map)
end end
def cast_and_apply(data) do def cast_and_apply(data) do
@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
def fix_attachments(data), do: data def fix_attachments(data), do: data
defp remote_mention_resolver(
%{"id" => ap_id, "tag" => tags},
"@" <> nickname = mention,
buffer,
opts,
acc
)
when is_binary(ap_id) and is_list(tags) do
initial_host =
ap_id
|> URI.parse()
|> Map.get(:host)
with mention_tag when not is_nil(mention_tag) <-
Enum.find(tags, &mention_tag?(&1, mention, initial_host)),
href when is_binary(href) <- mention_tag["href"],
%User{} = user <- User.get_cached_by_ap_id(href) do
link = Pleroma.Formatter.mention_from_user(user, opts)
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
else
_ -> {buffer, acc}
end
end
defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc}
defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host)
when is_binary(name) do
name == mention || mention == "#{name}@#{initial_host}"
end
defp mention_tag?(_tag, _mention, _initial_host), do: false
defp scrub_content(%{"content" => content} = object) when is_binary(content) do
Map.put(object, "content", HTML.filter_tags(content))
end
defp scrub_content(object), do: object
defp mfm_parse_limit do
min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit]))
end
defp normalize_source(%{"source" => source} = object) when is_binary(source) do
object
|> Map.put("source", %{"content" => source})
|> normalize_source()
end
defp normalize_source(%{"source" => source} = object) when is_map(source) do
source =
case source["content"] do
content when is_binary(content) ->
if String.length(content) <= mfm_parse_limit() do
source
else
Map.delete(source, "content")
end
nil ->
source
_ ->
Map.delete(source, "content")
end
Map.put(object, "source", source)
end
defp normalize_source(object), do: object
defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object)
when is_binary(content) do
Map.put(object, "content", HTML.filter_tags(content))
end
defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object
defp fix_misskey_content(
%{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object
)
when is_binary(content) do
mention_handler = fn nick, buffer, opts, acc ->
remote_mention_resolver(object, nick, buffer, opts, acc)
end
{linked, _mentions, _tags} =
Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
Map.put(object, "content", linked)
end
defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object),
do: scrub_content(object)
defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
object
|> Map.put("source", %{
"content" => content,
"mediaType" => "text/x.misskeymarkdown"
})
|> Map.delete("_misskey_content")
|> fix_misskey_content()
end
defp fix_misskey_content(object), do: object
defp fix(data) do defp fix(data) do
data data
|> CommonFixes.fix_actor() |> CommonFixes.fix_actor()
@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|> fix_tag() |> fix_tag()
|> fix_replies() |> fix_replies()
|> fix_attachments() |> fix_attachments()
|> normalize_source()
|> fix_misskey_content()
|> CommonFixes.fix_quote_url() |> CommonFixes.fix_quote_url()
|> CommonFixes.fix_likes() |> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji() |> Transmogrifier.fix_emoji()

View file

@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
quote bind_quoted: binding() do quote bind_quoted: binding() do
field(:content, :string) field(:content, :string)
field(:contentMap, ObjectValidators.ContentLanguageMap) field(:contentMap, ObjectValidators.ContentLanguageMap)
field(:htmlMfm, :boolean)
field(:published, ObjectValidators.DateTime) field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime) field(:updated, ObjectValidators.DateTime)

View file

@ -75,15 +75,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
end end
end end
# For remote Updates, verify the host is the same. # For remote Updates, verify the Actor is the same
def validate_updating_rights_remote(cng) do def validate_updating_rights_remote(cng) do
with actor = get_field(cng, :actor), with actor = get_field(cng, :actor),
object = get_field(cng, :object), object = get_field(cng, :object),
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object), {:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
actor_uri <- URI.parse(actor), entity <-
object_uri <- URI.parse(object_id), Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do
true <- actor_uri.host == object_uri.host do case entity do
cng # Actor must own Object to update it
%Object{} ->
if actor == entity.data["actor"] do
cng
else
cng
|> add_error(:object, "Can't be updated by this actor")
end
# Actor must only be allowed to update itself
%User{} ->
if actor == entity.ap_id do
cng
else
cng
|> add_error(:object, "Can't be updated by this actor")
end
nil ->
cng
|> add_error(:object, "Can't be updated by this actor")
_ ->
cng
|> add_error(:object, "Update is neither for Object or Actor")
end
else else
_e -> _e ->
cng cng

View file

@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld", "#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{ %{
"@language" => get_language(data) "@language" => get_language(data),
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
} }
] ]
} }

View file

@ -317,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
emoji = Map.merge(emoji, summary_emoji) emoji = Map.merge(emoji, summary_emoji)
media_type = Utils.get_content_type(draft.params[:content_type])
{:ok, note_data, _meta} = Builder.note(draft) {:ok, note_data, _meta} = Builder.note(draft)
object = object =
@ -324,14 +325,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Map.put("emoji", emoji) |> Map.put("emoji", emoji)
|> Map.put("source", %{ |> Map.put("source", %{
"content" => draft.status, "content" => draft.status,
"mediaType" => Utils.get_content_type(draft.params[:content_type]) "mediaType" => media_type
}) })
|> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown")
|> Map.put("generator", draft.params[:generator]) |> Map.put("generator", draft.params[:generator])
|> Map.put("language", draft.language) |> Map.put("language", draft.language)
%{draft | object: object} %{draft | object: object}
end end
defp maybe_put(map, key, value, true), do: Map.put(map, key, value)
defp maybe_put(map, _key, _value, _condition), do: map
defp preview?(%__MODULE__{} = draft) do defp preview?(%__MODULE__{} = draft) do
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
%{draft | preview?: preview?} %{draft | preview?: preview?}

View file

@ -322,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.linkify(options) |> Formatter.linkify(options)
end end
def format_input(text, "text/x.misskeymarkdown", options) do
text
|> Formatter.markdown_to_html(%{breaks: true})
|> safe_mfm_to_html()
|> Formatter.linkify(options)
|> Formatter.html_escape("text/x.misskeymarkdown")
end
def format_input(text, "text/markdown", options) do def format_input(text, "text/markdown", options) do
text text
|> Formatter.mentions_escape(options) |> Formatter.mentions_escape(options)
@ -330,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.html_escape("text/html") |> Formatter.html_escape("text/html")
end end
defp safe_mfm_to_html(html) do
html
|> MfmParser.Parser.parse()
|> MfmParser.Encoder.to_html()
rescue
_ -> html
catch
_, _ -> html
end
def format_naive_asctime(date) do def format_naive_asctime(date) do
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
end end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Workers.PublisherWorker alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker alias Pleroma.Workers.ReceiverWorker
alias Pleroma.Workers.SignatureRetryWorker
require Logger require Logger
@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do
end end
# Client API # Client API
def incoming_ap_doc(%{params: params, req_headers: req_headers}) do def incoming_failed_signature_ap_doc(%{
ReceiverWorker.new( method: method,
params: params,
req_headers: req_headers,
request_path: request_path,
query_string: query_string
}) do
SignatureRetryWorker.new(
%{ %{
"op" => "incoming_ap_doc", "op" => "incoming_failed_signature_ap_doc",
"method" => method,
"req_headers" => req_headers, "req_headers" => req_headers,
"params" => params, "params" => params,
"request_path" => request_path,
"query_string" => query_string,
"timeout" => :timer.seconds(20) "timeout" => :timer.seconds(20)
}, },
priority: 2 priority: 2

View file

@ -71,12 +71,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end) end)
end end
defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters
defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
length(voters) length(voters)
end end
defp voters_count(%{data: %{"votersCount" => voters}}), do: voters
defp voters_count(_), do: 0 defp voters_count(_), do: 0
defp voted_and_own_votes(%{object: object} = params, options) do defp voted_and_own_votes(%{object: object} = params, options) do

View file

@ -32,8 +32,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
# remove me once testsuite uses mapped capabilities instead of what we do now # remove me once testsuite uses mapped capabilities instead of what we do now
{:user, nil} -> {:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)") Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}") Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
conn assign(conn, :valid_signature, false)
end end
end end

View file

@ -4,40 +4,37 @@
defmodule Pleroma.Workers.ReceiverWorker do defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Workers.SignatureRetryWorker
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity] use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true @impl true
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params} = args} = job) do
if signature_retry_job?(args) do
perform_signature_retry(job)
else
perform_incoming(params)
end
end
def perform(%Job{ def perform(%Job{args: %{"op" => "incoming_ap_doc"} = args} = job) do
args: %{ if signature_retry_job?(args) do
"op" => "incoming_ap_doc", perform_signature_retry(job)
"method" => method, else
"params" => params, process_errors(:missing_incoming_ap_doc_params)
"req_headers" => req_headers, end
"request_path" => request_path, end
"query_string" => query_string
}
}) do
# Oban's serialization converts our tuple headers to lists.
# Revert it for the signature validation.
req_headers = Enum.into(req_headers, [], &List.to_tuple(&1))
conn_data = %Plug.Conn{ defp perform_signature_retry(%Job{args: args} = job) do
method: method, SignatureRetryWorker.perform(%Job{
params: params, job
req_headers: req_headers, | args: Map.put(args, "op", "incoming_failed_signature_ap_doc")
request_path: request_path, })
query_string: query_string end
}
with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), defp perform_incoming(params) do
{:ok, _public_key} <- Signature.refetch_public_key(conn_data), with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
{:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
unless Instances.reachable?(params["actor"]) do unless Instances.reachable?(params["actor"]) do
domain = URI.parse(params["actor"]).host domain = URI.parse(params["actor"]).host
Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
@ -49,17 +46,8 @@ defmodule Pleroma.Workers.ReceiverWorker do
end end
end end
def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do defp signature_retry_job?(args) do
with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do Enum.any?(~w(method req_headers request_path query_string), &Map.has_key?(args, &1))
unless Instances.reachable?(params["actor"]) do
domain = URI.parse(params["actor"]).host
Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
end
{:ok, res}
else
e -> process_errors(e)
end
end end
@impl true @impl true
@ -85,10 +73,12 @@ defmodule Pleroma.Workers.ReceiverWorker do
{:error, {:reject, _} = reason} -> {:cancel, reason} {:error, {:reject, _} = reason} -> {:cancel, reason}
# HTTP Sigs # HTTP Sigs
{:signature, false} -> {:cancel, :invalid_signature} {:signature, false} -> {:cancel, :invalid_signature}
{:same_actor, false} -> {:cancel, :actor_signature_mismatch}
# Origin / URL validation failed somewhere possibly due to spoofing # Origin / URL validation failed somewhere possibly due to spoofing
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} {:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
# Unclear if this can be reached # Unclear if this can be reached
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason} {:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
:missing_incoming_ap_doc_params -> {:cancel, :missing_incoming_ap_doc_params}
# Catchall # Catchall
{:error, _} = e -> e {:error, _} = e -> e
e -> {:error, e} e -> {:error, e}

View file

@ -0,0 +1,254 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.SignatureRetryWorker do
alias Pleroma.Instances
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Federator
alias Pleroma.Web.Plugs.MappedSignatureToIdentityPlug
require Logger
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true
def perform(%Job{
args: %{
"op" => "incoming_failed_signature_ap_doc",
"method" => method,
"params" => params,
"req_headers" => req_headers,
"request_path" => request_path,
"query_string" => query_string
}
})
when is_binary(method) and is_map(params) and is_list(req_headers) and
is_binary(request_path) and is_binary(query_string) do
case normalize_req_headers(req_headers) do
{:ok, req_headers} ->
conn_data = %Plug.Conn{
assigns: %{valid_signature: true},
method: method,
params: params,
req_headers: req_headers,
request_path: request_path,
query_string: query_string
}
signature_actor_result = signature_actor_id(conn_data)
with actor_id = Utils.get_ap_id(params["actor"]),
{:signature_actor, {:ok, signature_actor_id}} <-
{:signature_actor, signature_actor_result},
{:same_actor, true} <- {:same_actor, signature_actor_id == actor_id},
{:ok, %User{}} <- User.get_or_fetch_by_ap_id(actor_id),
{:ok, _public_key} <- Signature.refetch_public_key(conn_data),
{:signature, true} <- {:signature, validate_signature(conn_data)},
{:same_actor, true} <- {:same_actor, validate_same_actor(conn_data)},
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
unless Instances.reachable?(params["actor"]) do
domain = URI.parse(params["actor"]).host
Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}))
end
{:ok, res}
else
e -> process_errors(e, retry_log_context(params, request_path, signature_actor_result))
end
e ->
process_errors(e, retry_log_context(params, request_path, nil))
end
end
def perform(%Job{args: %{"op" => "incoming_failed_signature_ap_doc"} = args}) do
process_errors(
:missing_signature_retry_metadata,
retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
)
end
def perform(%Job{args: args}) when is_map(args) do
process_errors(
:missing_signature_retry_metadata,
retry_log_context(Map.get(args, "params"), Map.get(args, "request_path"), nil)
)
end
def perform(%Job{}), do: process_errors(:missing_signature_retry_metadata)
@impl true
def timeout(%_{args: %{"timeout" => timeout}}), do: timeout
def timeout(_job), do: :timer.seconds(5)
defp normalize_req_headers(req_headers) do
req_headers
|> Enum.reduce_while({:ok, []}, fn
{key, value}, {:ok, acc} when is_binary(key) and is_binary(value) ->
{:cont, {:ok, [{key, value} | acc]}}
[key, value], {:ok, acc} when is_binary(key) and is_binary(value) ->
{:cont, {:ok, [{key, value} | acc]}}
_, _ ->
{:halt, {:error, :invalid_signature_retry_metadata}}
end)
|> case do
{:ok, headers} -> {:ok, Enum.reverse(headers)}
error -> error
end
end
defp validate_same_actor(conn_data) do
case MappedSignatureToIdentityPlug.call(conn_data, []) do
%Plug.Conn{assigns: %{valid_signature: true}} ->
true
_ ->
false
end
end
defp validate_signature(conn_data) do
Signature.validate_signature(conn_data)
rescue
_ -> false
catch
_, _ -> false
end
defp signature_actor_id(conn_data) do
Signature.get_actor_id(conn_data)
rescue
_ -> {:error, :invalid_signature}
catch
_, _ -> {:error, :invalid_signature}
end
defp process_errors(errors, context \\ %{})
defp process_errors({:error, {:error, _} = error}, context), do: process_errors(error, context)
defp process_errors(errors, context) do
result =
case errors do
# User fetch failures
{:error, :not_found} = reason ->
{:cancel, reason}
{:error, :forbidden} = reason ->
{:cancel, reason}
# Inactive user
{:error, {:user_active, false} = reason} ->
{:cancel, reason}
# Validator will error and return a changeset error
# e.g., duplicate activities or if the object was deleted
{:error, {:validate, {:error, _changeset} = reason}} ->
{:cancel, reason}
# Duplicate detection during Normalization
{:error, :already_present} ->
{:cancel, :already_present}
# MRFs will return a reject
{:error, {:reject, _} = reason} ->
{:cancel, reason}
# HTTP Sigs
{:signature_actor, {:error, _}} ->
{:cancel, :invalid_signature}
{:signature, false} ->
{:cancel, :invalid_signature}
{:same_actor, false} ->
{:cancel, :actor_signature_mismatch}
# Origin / URL validation failed somewhere possibly due to spoofing
{:error, :origin_containment_failed} ->
{:cancel, :origin_containment_failed}
# Unclear if this can be reached
{:error, {:side_effects, {:error, :no_object_actor}} = reason} ->
{:cancel, reason}
# Fail closed if the retry cannot reconstruct the original request.
:missing_signature_retry_metadata ->
{:cancel, :missing_signature_retry_metadata}
{:error, :invalid_signature_retry_metadata} ->
{:cancel, :invalid_signature_retry_metadata}
# Catchall
{:error, _} = e ->
e
e ->
{:error, e}
end
log_signature_retry_rejection(result, context)
result
end
defp retry_log_context(params, request_path, signature_actor_result) when is_map(params) do
signature_actor =
case signature_actor_result do
{:ok, actor} when is_binary(actor) -> actor
actor when is_binary(actor) -> actor
_ -> nil
end
%{
activity_id: params["id"],
payload_actor: Utils.get_ap_id(params["actor"]),
request_path: request_path,
signature_actor: signature_actor,
type: params["type"]
}
end
defp retry_log_context(_params, request_path, signature_actor_result) do
signature_actor =
case signature_actor_result do
{:ok, actor} when is_binary(actor) -> actor
actor when is_binary(actor) -> actor
_ -> nil
end
%{
activity_id: nil,
payload_actor: nil,
request_path: request_path,
signature_actor: signature_actor,
type: nil
}
end
defp log_signature_retry_rejection({:cancel, reason}, context)
when reason in [
:actor_signature_mismatch,
:invalid_signature,
:invalid_signature_retry_metadata,
:missing_signature_retry_metadata,
:origin_containment_failed
] do
Logger.warning(
"Failed-signature inbox retry rejected " <>
"reason=#{inspect(reason)} " <>
"payload_actor=#{inspect(context[:payload_actor])} " <>
"signature_actor=#{inspect(context[:signature_actor])} " <>
"activity_id=#{inspect(context[:activity_id])} " <>
"type=#{inspect(context[:type])} " <>
"request_path=#{inspect(context[:request_path])}"
)
end
defp log_signature_retry_rejection(_result, _context), do: :ok
end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do def project do
[ [
app: :pleroma, app: :pleroma,
version: version("2.10.0"), version: version("2.10.2"),
elixir: "~> 1.15", elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(), compilers: Mix.compilers(),
@ -160,6 +160,9 @@ defmodule Pleroma.Mixfile do
{:sweet_xml, "~> 0.7.5"}, {:sweet_xml, "~> 0.7.5"},
{:earmark, "1.4.46"}, {:earmark, "1.4.46"},
{:bbcode_pleroma, "~> 0.2.0"}, {:bbcode_pleroma, "~> 0.2.0"},
{:mfm_parser,
git: "https://akkoma.dev/AkkomaGang/mfm-parser.git",
ref: "360a30267a847810a63ab48f606ba227b2ca05f0"},
{:cors_plug, "~> 2.0"}, {:cors_plug, "~> 2.0"},
{:web_push_encryption, "~> 0.3.1"}, {:web_push_encryption, "~> 0.3.1"},
{:swoosh, "~> 1.16.12"}, {:swoosh, "~> 1.16.12"},

View file

@ -79,6 +79,7 @@
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mfm_parser": {:git, "https://akkoma.dev/AkkomaGang/mfm-parser.git", "360a30267a847810a63ab48f606ba227b2ca05f0", [ref: "360a30267a847810a63ab48f606ba227b2ca05f0"]},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},

View file

@ -82,12 +82,50 @@ defmodule Pleroma.HTML.Scrubber.Default do
"recipients-inline", "recipients-inline",
"quote-inline", "quote-inline",
"invisible", "invisible",
"ellipsis" "ellipsis",
"mfm-center",
"mfm-flip",
"mfm-font",
"mfm-blur",
"mfm-rotate",
"mfm-x2",
"mfm-x3",
"mfm-x4",
"mfm-position",
"mfm-scale",
"mfm-fg",
"mfm-bg",
"mfm-jelly",
"mfm-twitch",
"mfm-shake",
"mfm-spin",
"mfm-jump",
"mfm-bounce",
"mfm-rainbow",
"mfm-tada",
"mfm-sparkle"
]) ])
Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"]) Meta.allow_tag_with_this_attribute_values(:p, "class", ["quote-inline"])
Meta.allow_tag_with_these_attributes(:span, ["lang"]) Meta.allow_tag_with_these_attributes(:span, [
"lang",
"data-mfm-h",
"data-mfm-v",
"data-mfm-x",
"data-mfm-y",
"data-mfm-alternate",
"data-mfm-speed",
"data-mfm-deg",
"data-mfm-left",
"data-mfm-serif",
"data-mfm-monospace",
"data-mfm-cursive",
"data-mfm-fantasy",
"data-mfm-emoji",
"data-mfm-math",
"data-mfm-color"
])
Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"]) Meta.allow_tag_with_this_attribute_values(:code, "class", ["inline"])

View file

@ -4,46 +4,55 @@
"https://w3id.org/security/v1", "https://w3id.org/security/v1",
"https://purl.archive.org/socialweb/webfinger", "https://purl.archive.org/socialweb/webfinger",
{ {
"Emoji": "toot:Emoji", "as": "https://www.w3.org/ns/activitystreams#",
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"vcard": "http://www.w3.org/2006/vcard/ns#",
"fedibird": "http://fedibird.com/ns#",
"litepub": "http://litepub.social/ns#",
"sm": "http://smithereen.software/ns#",
"toot": "http://joinmastodon.org/ns#",
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"Hashtag": "as:Hashtag", "Hashtag": "as:Hashtag",
"PropertyValue": "schema:PropertyValue", "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"quoteUrl": "as:quoteUrl",
"sensitive": "as:sensitive",
"atomUri": "ostatus:atomUri", "atomUri": "ostatus:atomUri",
"conversation": { "conversation": {
"@id": "ostatus:conversation", "@id": "ostatus:conversation",
"@type": "@id" "@type": "@id"
}, },
"discoverable": "toot:discoverable",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers", "PropertyValue": "schema:PropertyValue",
"capabilities": "litepub:capabilities",
"ostatus": "http://ostatus.org#",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#",
"fedibird": "http://fedibird.com/ns#",
"value": "schema:value", "value": "schema:value",
"sensitive": "as:sensitive",
"litepub": "http://litepub.social/ns#", "quoteUri": "fedibird:quoteUri",
"invisible": "litepub:invisible",
"capabilities": "litepub:capabilities",
"ChatMessage": "litepub:ChatMessage",
"directMessage": "litepub:directMessage", "directMessage": "litepub:directMessage",
"EmojiReact": "litepub:EmojiReact",
"formerRepresentations": "litepub:formerRepresentations",
"invisible": "litepub:invisible",
"listMessage": { "listMessage": {
"@id": "litepub:listMessage", "@id": "litepub:listMessage",
"@type": "@id" "@type": "@id"
}, },
"quoteUrl": "as:quoteUrl",
"quoteUri": "fedibird:quoteUri",
"oauthRegistrationEndpoint": { "oauthRegistrationEndpoint": {
"@id": "litepub:oauthRegistrationEndpoint", "@id": "litepub:oauthRegistrationEndpoint",
"@type": "@id" "@type": "@id"
}, },
"EmojiReact": "litepub:EmojiReact",
"ChatMessage": "litepub:ChatMessage", "nonAnonymous": "sm:nonAnonymous",
"alsoKnownAs": {
"@id": "as:alsoKnownAs", "discoverable": "toot:discoverable",
"@type": "@id" "Emoji": "toot:Emoji"
},
"vcard": "http://www.w3.org/2006/vcard/ns#",
"formerRepresentations": "litepub:formerRepresentations",
"sm": "http://smithereen.software/ns#",
"nonAnonymous": "sm:nonAnonymous"
} }
] ]
} }

View file

@ -78,14 +78,19 @@ update() {
RELEASE_ROOT=$(dirname "$SCRIPTPATH") RELEASE_ROOT=$(dirname "$SCRIPTPATH")
uri="https://git.pleroma.social" uri="https://git.pleroma.social"
project_id="2" project_name="pleroma"
project_branch="${BRANCH:-$(detect_branch)}" package_base="${uri}/api/packages/${project_name}/generic"
flavour="${FLAVOUR:-$(detect_flavour)}" if [ -n "$FULL_URI" ]; then
full_uri="$FULL_URI"
else
project_branch="${BRANCH:-$(detect_branch)}"
flavour="${FLAVOUR:-$(detect_flavour)}"
full_uri="${package_base}"/pleroma-otp-"${project_branch}"-"${flavour}"/latest/pleroma.zip
fi
tmp="${TMP_DIR:-/tmp}" tmp="${TMP_DIR:-/tmp}"
artifact="$tmp/pleroma.zip" artifact="$tmp/pleroma.zip"
full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}"
echo "Downloading the artifact from ${full_uri} to ${artifact}" echo "Downloading the artifact from ${full_uri} to ${artifact}"
curl "$full_uri" -o "${artifact}" curl -fL "$full_uri" -o "${artifact}"
echo "Unpacking ${artifact} to ${tmp}" echo "Unpacking ${artifact} to ${tmp}"
unzip -q "$artifact" -d "$tmp" unzip -q "$artifact" -d "$tmp"
echo "Copying files over to $RELEASE_ROOT" echo "Copying files over to $RELEASE_ROOT"
@ -137,7 +142,14 @@ else
SCRIPT=$(realpath "$0") SCRIPT=$(realpath "$0")
SCRIPTPATH=$(dirname "$SCRIPT") SCRIPTPATH=$(dirname "$SCRIPT")
FULL_ARGS="$*" # HACK: Script arguments need to be sent as an array to Mix tasks, otherwise they will break (quoted) arguments with whitespace.
# Previously it was sent as string, which would get split on whitespace on the task side.
# Encode as Elixir binary literals to avoid string escaping and interpolation issues.
PREPARED_ARGS=""
for arg in "$@"; do
bytes=$(printf '%s' "$arg" | od -An -v -tu1 | tr -s '[:space:]' ',' | sed 's/^,//; s/,$//')
PREPARED_ARGS="$PREPARED_ARGS <<$bytes>>,"
done
ACTION="$1" ACTION="$1"
if [ $# -gt 0 ]; then if [ $# -gt 0 ]; then
@ -154,8 +166,8 @@ else
if [ "$ACTION" = "update" ]; then if [ "$ACTION" = "update" ]; then
update "$@" update "$@"
elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then
"$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' "$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])'
else else
"$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")' "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run(['"${PREPARED_ARGS%%,}"'])'
fi fi
fi fi

View file

@ -144,7 +144,13 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
quarantined_instances: [], quarantined_instances: [],
managed_config: true, managed_config: true,
static_dir: "instance/static/", static_dir: "instance/static/",
allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"], allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
],
autofollowed_nicknames: [], autofollowed_nicknames: [],
max_pinned_statuses: 1, max_pinned_statuses: 1,
attachment_links: false, attachment_links: false,

View file

@ -0,0 +1,293 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.PleromaCtlTest do
use ExUnit.Case, async: false
@pleroma_ctl Path.expand("../../rel/files/bin/pleroma_ctl", __DIR__)
setup do
tmp_dir =
Path.join(System.tmp_dir!(), "pleroma_ctl_test_#{System.unique_integer([:positive])}")
release_root = Path.join(tmp_dir, "release")
bin_dir = Path.join(release_root, "bin")
File.mkdir_p!(bin_dir)
File.cp!(@pleroma_ctl, Path.join(bin_dir, "pleroma_ctl"))
File.chmod!(Path.join(bin_dir, "pleroma_ctl"), 0o755)
on_exit(fn -> File.rm_rf!(tmp_dir) end)
{:ok, tmp_dir: tmp_dir, release_root: release_root, bin_dir: bin_dir}
end
test "update downloads branch-scoped latest OTP package", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "unused", "glibc")
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
File.mkdir_p!(update_tmp_dir)
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
[
"update",
"--branch",
"develop",
"--flavour",
"amd64",
"--tmp-dir",
update_tmp_dir,
"--no-rm"
],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert File.exists?(Path.join(release_root, "bin/marker"))
assert ["-fL", url, "-o", artifact_path] =
curl_args_path
|> File.read!()
|> String.split("\n", trim: true)
assert url ==
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-amd64/latest/pleroma.zip"
assert artifact_path == Path.join(update_tmp_dir, "pleroma.zip")
end
test "update detects stable branch and local flavour", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "x86_64", "glibc")
write_start_erl_data(release_root, "2.10.0")
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert curl_url(curl_args_path) ==
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-stable-amd64/latest/pleroma.zip"
end
test "update detects develop branch and musl arm flavour", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "armv7l", "musl")
write_start_erl_data(release_root, "2.10.0.develop")
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
["update", "--tmp-dir", create_update_tmp_dir(tmp_dir), "--no-rm"],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert curl_url(curl_args_path) ==
"https://git.pleroma.social/api/packages/pleroma/generic/pleroma-otp-develop-arm-musl/latest/pleroma.zip"
end
test "update with zip URL bypasses branch and flavour detection", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir,
release_root: release_root
} do
stubs_dir = Path.join(tmp_dir, "stubs")
curl_args_path = Path.join(tmp_dir, "curl.args")
File.mkdir_p!(stubs_dir)
write_update_stubs(stubs_dir, curl_args_path, "unsupported-arch", "unsupported-libc")
write_start_erl_data(release_root, "2.10.0.custombranch")
custom_url = "https://example.test/custom.zip"
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
[
"update",
"--zip-url",
custom_url,
"--tmp-dir",
create_update_tmp_dir(tmp_dir),
"--no-rm"
],
env: [{"PATH", stubs_dir <> ":" <> System.get_env("PATH", "")}],
stderr_to_stdout: true
)
assert status == 0, output
assert curl_url(curl_args_path) == custom_url
end
test "passes arguments with spaces and Elixir string metacharacters", %{
tmp_dir: tmp_dir,
bin_dir: bin_dir
} do
capture_path = Path.join(tmp_dir, "captured_args")
eval_path = Path.join(tmp_dir, "pleroma_ctl_eval.exs")
write_executable(Path.join(bin_dir, "pleroma"), """
#!/bin/sh
{
printf '%s\n' 'defmodule Pleroma.ReleaseTasks do'
printf '%s\n' ' def run(args), do: File.write!(System.fetch_env!("PLEROMA_CTL_CAPTURE"), :erlang.term_to_binary(args))'
printf '%s\n' 'end'
printf '%s\n' "$2"
} > "$PLEROMA_CTL_EVAL_FILE"
exec elixir "$PLEROMA_CTL_EVAL_FILE"
""")
{output, status} =
System.cmd(
Path.join(bin_dir, "pleroma_ctl"),
[
"user",
"",
"has space",
~s(has "quote"),
~s(has \\ backslash),
~S(#{:not_interpolated})
],
env: [
{"PLEROMA_CTL_CAPTURE", capture_path},
{"PLEROMA_CTL_EVAL_FILE", eval_path}
],
stderr_to_stdout: true
)
assert status == 0, output
assert capture_path
|> File.read!()
|> :erlang.binary_to_term() == [
"user",
"",
"has space",
~s(has "quote"),
~s(has \\ backslash),
~S(#{:not_interpolated})
]
end
defp write_executable(path, contents) do
File.write!(path, contents)
File.chmod!(path, 0o755)
end
defp write_start_erl_data(release_root, version) do
releases_dir = Path.join(release_root, "releases")
File.mkdir_p!(releases_dir)
File.write!(Path.join(releases_dir, "start_erl.data"), "erts-15.0 #{version}\n")
end
defp create_update_tmp_dir(tmp_dir) do
update_tmp_dir = Path.join(tmp_dir, "update_tmp")
File.mkdir_p!(update_tmp_dir)
update_tmp_dir
end
defp write_update_stubs(stubs_dir, curl_args_path, arch, libc) do
write_executable(Path.join(stubs_dir, "curl"), """
#!/bin/sh
printf '%s\n' "$@" > "#{curl_args_path}"
while [ $# -gt 0 ]; do
case "$1" in
-o)
artifact="$2"
shift 2
;;
*)
shift
;;
esac
done
: > "$artifact"
""")
write_executable(Path.join(stubs_dir, "unzip"), """
#!/bin/sh
while [ $# -gt 0 ]; do
case "$1" in
-d)
dest="$2"
shift 2
;;
*)
shift
;;
esac
done
mkdir -p "$dest/release/bin"
printf 'marker' > "$dest/release/bin/marker"
""")
write_executable(Path.join(stubs_dir, "uname"), """
#!/bin/sh
printf '%s\n' '#{arch}'
""")
write_executable(Path.join(stubs_dir, "getconf"), getconf_stub(libc))
write_executable(Path.join(stubs_dir, "ldd"), """
#!/bin/sh
printf '%s\n' 'musl libc (mock)'
""")
end
defp getconf_stub("glibc") do
"""
#!/bin/sh
printf '%s\n' 'glibc 2.40'
"""
end
defp getconf_stub(_libc) do
"""
#!/bin/sh
exit 1
"""
end
defp curl_url(curl_args_path) do
["-fL", url, "-o", _artifact_path] =
curl_args_path
|> File.read!()
|> String.split("\n", trim: true)
url
end
end

View file

@ -876,17 +876,17 @@ defmodule Pleroma.UserTest do
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true) setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
test "for mastodon" do test "fetches a mastodon split-domain nickname" do
ap_id = "a@mastodon.example" nickname = "a@mastodon.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id) {:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a" assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
assert fetched_user.nickname == "a@mastodon.example" assert fetched_user.nickname == "a@mastodon.example"
end end
test "for pleroma" do test "fetches a pleroma split-domain nickname" do
ap_id = "a@pleroma.example" nickname = "a@pleroma.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id) {:ok, fetched_user} = User.get_or_fetch(nickname)
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a" assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
assert fetched_user.nickname == "a@pleroma.example" assert fetched_user.nickname == "a@pleroma.example"
@ -936,6 +936,89 @@ defmodule Pleroma.UserTest do
assert fetched_user == "not found nonexistent" assert fetched_user == "not found nonexistent"
end end
test "does not rename an existing remote actor from rogue WebFinger data" do
clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
actor_id = "https://legit-actor.example/users/alice"
Tesla.Mock.mock(fn
%{url: "https://evil-webfinger.example/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
%{
url:
"https://evil-webfinger.example/.well-known/webfinger?resource=acct:claimed@evil-webfinger.example"
} ->
Tesla.Mock.json(%{
"subject" => "acct:claimed@evil-webfinger.example",
"links" => [
%{
"rel" => "self",
"type" => "application/activity+json",
"href" => actor_id
}
]
})
%{url: ^actor_id} ->
{:ok,
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
Jason.encode!(%{
"id" => actor_id,
"type" => "Person",
"preferredUsername" => "alice",
"name" => "Alice",
"summary" => "",
"inbox" => "https://legit-actor.example/users/alice/inbox",
"outbox" => "https://legit-actor.example/users/alice/outbox",
"followers" => "https://legit-actor.example/users/alice/followers",
"following" => "https://legit-actor.example/users/alice/following"
})
}}
%{url: "https://legit-actor.example/.well-known/host-meta"} ->
{:ok, %Tesla.Env{status: 404}}
%{
url:
"https://legit-actor.example/.well-known/webfinger?resource=acct:alice@legit-actor.example"
} ->
Tesla.Mock.json(%{
"subject" => "acct:alice@legit-actor.example",
"links" => [
%{
"rel" => "self",
"type" => "application/activity+json",
"href" => actor_id
}
]
})
end)
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
refute User.get_by_ap_id(actor_id)
refute User.get_by_nickname("claimed@evil-webfinger.example")
orig_user =
insert(:user,
local: false,
nickname: "alice@legit-actor.example",
ap_id: actor_id
)
assert {:error, {:webfinger_actor_mismatch, "claimed@evil-webfinger.example", ^actor_id}} =
ActivityPub.make_user_from_nickname("claimed@evil-webfinger.example")
assert {:error, _} = User.get_or_fetch_by_nickname("claimed@evil-webfinger.example")
assert User.get_by_id(orig_user.id).nickname == "alice@legit-actor.example"
refute User.get_by_nickname("claimed@evil-webfinger.example")
end
test "updates an existing user, if stale" do test "updates an existing user, if stale" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800) a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)

View file

@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Workers.ReceiverWorker alias Pleroma.Workers.ReceiverWorker
alias Pleroma.Workers.SignatureRetryWorker
import Pleroma.Factory import Pleroma.Factory
@ -36,6 +37,36 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
setup do: clear_config([:instance, :federating], true) setup do: clear_config([:instance, :federating], true)
defp assign_valid_signature_for_actor(conn, %User{ap_id: actor_id}) do
assign_valid_signature_for_actor(conn, actor_id)
end
defp assign_valid_signature_for_actor(conn, actor) do
actor_id = Utils.get_ap_id(actor)
conn
|> assign(:valid_signature, true)
|> put_req_header("signature", "keyId=\"#{actor_id}#main-key\"")
end
defp expect_signature_retry_from(%User{} = signer) do
signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
Tesla.Mock.mock(fn
%{url: url} when url == signer.ap_id ->
%Tesla.Env{
status: 200,
body: Jason.encode!(signer_json),
headers: HttpRequestMock.activitypub_object_headers()
}
env ->
apply(HttpRequestMock, :request, [env])
end)
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
end
describe "/relay" do describe "/relay" do
setup do: clear_config([:instance, :allow_relay]) setup do: clear_config([:instance, :allow_relay])
@ -688,7 +719,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
@ -716,7 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
@ -726,6 +757,199 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"]) assert Activity.get_by_ap_id(data["id"])
end end
test "does not create a forged post after failed signature retry", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/inbox-forged-note"
data = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-forged-create",
"context" => "https://two.com/contexts/inbox-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/inbox-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
expect_signature_retry_from(alice)
conn =
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
refute Object.get_by_ap_id(object_id)
end
test "does not create a forged like after failed signature retry", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
data = %{
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
expect_signature_retry_from(alice)
conn =
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
end
test "does not delete an object after failed signature retry", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
object_id = note.data["id"]
data = %{
"type" => "Delete",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-forged-delete",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => object_id
}
expect_signature_retry_from(alice)
conn =
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("signature", "keyId=\"https://one.com/users/alice#main-key\"")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
assert %Object{data: %{"type" => "Note"}} = Object.get_by_ap_id(object_id)
end
test "does not create a forged post signed by a different actor", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/inbox-signed-forged-note"
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-signed-forged-create",
"context" => "https://two.com/contexts/inbox-signed-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/inbox-signed-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
expect_signature_retry_from(alice)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|> put_req_header("digest", "SHA-256=fake-digest")
|> put_req_header(
"signature",
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
)
|> post("/inbox", data)
assert conn.assigns.valid_signature == false
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
refute Object.get_by_ap_id(object_id)
end
test "does not create a forged like signed by a different actor", %{conn: conn} do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/inbox-signed-forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
expect_signature_retry_from(alice)
conn =
conn
|> put_req_header("content-type", "application/activity+json")
|> put_req_header("date", "Thu, 25 Jul 2024 13:33:31 GMT")
|> put_req_header("digest", "SHA-256=fake-digest")
|> put_req_header(
"signature",
"keyId=\"#{alice.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
)
|> post("/inbox", data)
assert conn.assigns.valid_signature == false
assert "ok" == json_response(conn, 200)
assert [{:cancel, :actor_signature_mismatch}] =
ObanHelpers.perform(all_enqueued(worker: SignatureRetryWorker))
refute Activity.get_by_ap_id(data["id"])
end
test "accept follow activity", %{conn: conn} do test "accept follow activity", %{conn: conn} do
clear_config([:instance, :federating], true) clear_config([:instance, :federating], true)
relay = Relay.get_actor() relay = Relay.get_actor()
@ -742,7 +966,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == assert "ok" ==
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(followed_relay)
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", accept) |> post("/inbox", accept)
|> json_response(200) |> json_response(200)
@ -822,16 +1046,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "Unknown activity types are discarded", %{conn: conn} do test "Unknown activity types are discarded", %{conn: conn} do
unknown_types = ["Poke", "Read", "Dazzle"] unknown_types = ["Poke", "Read", "Dazzle"]
actor =
insert(:user, local: false, ap_id: "https://unknown.mastodon.instance/users/somebody")
Enum.each(unknown_types, fn bad_type -> Enum.each(unknown_types, fn bad_type ->
params = params =
%{ %{
"type" => bad_type, "type" => bad_type,
"actor" => "https://unknown.mastodon.instance/users/somebody" "actor" => actor.ap_id
} }
|> Jason.encode!() |> Jason.encode!()
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(actor)
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params) |> post("/inbox", params)
|> json_response(400) |> json_response(400)
@ -900,7 +1127,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == assert "ok" ==
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
|> json_response(200) |> json_response(200)
@ -921,7 +1148,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == assert "ok" ==
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
|> json_response(200) |> json_response(200)
@ -990,7 +1217,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == assert "ok" ==
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
|> json_response(200) |> json_response(200)
@ -1009,7 +1236,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert "ok" == assert "ok" ==
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data) |> post("/inbox", data)
|> json_response(200) |> json_response(200)
@ -1040,7 +1267,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1061,7 +1288,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1082,7 +1309,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1106,7 +1333,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1133,7 +1360,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1163,7 +1390,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data) |> post("/users/#{recipient.nickname}/inbox", data)
@ -1228,7 +1455,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
} }
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data) |> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200) |> json_response(200)
@ -1318,7 +1545,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
} }
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data) |> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200) |> json_response(200)
@ -1372,7 +1599,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
} }
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data) |> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200) |> json_response(200)
@ -1405,7 +1632,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1428,7 +1655,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -1451,7 +1678,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn = conn =
conn conn
|> assign(:valid_signature, true) |> assign_valid_signature_for_actor(data["actor"])
|> put_req_header("content-type", "application/activity+json") |> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data) |> post("/users/#{user.nickname}/inbox", data)
@ -2571,6 +2798,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
setup do: clear_config([:media_proxy]) setup do: clear_config([:media_proxy])
setup do: clear_config([Pleroma.Upload]) setup do: clear_config([Pleroma.Upload])
# majic's libmagic port is unavailable on local Darwin runs; Linux CI still runs this test.
@tag :skip_darwin
test "POST /api/ap/upload_media", %{conn: conn} do test "POST /api/ap/upload_media", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -149,6 +149,171 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note) %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end end
test "a Misskey MFM note is rendered from source content" do
user = insert(:user, ap_id: "https://misskey.example/users/alice")
note = %{
"id" => "https://misskey.example/notes/1",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => "$[spin.speed=1s mfm goes here] <script>alert('xss')</script>",
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert source["mediaType"] == "text/x.misskeymarkdown"
assert content =~ ~s(class="mfm-spin")
assert content =~ ~s(data-mfm-speed="1s")
assert content =~ "mfm goes here"
refute content =~ "original content"
refute content =~ "<script"
end
test "a Misskey MFM note resolves only cached AP mention tags" do
remote_user = insert(:user, ap_id: "https://misskey.example/users/carol")
local_user = insert(:user, nickname: "local_user")
note = %{
"id" => "https://misskey.example/notes/3",
"type" => "Note",
"actor" => remote_user.ap_id,
"attributedTo" => remote_user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"tag" => [
%{
"type" => "Mention",
"name" => "@local_user",
"href" => local_user.ap_id
},
%{
"type" => "Mention",
"name" => "@uncached",
"href" => "https://misskey.example/users/uncached"
}
],
"source" => %{
"content" => "@local_user @uncached $[spin hello]",
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content =~ local_user.ap_id
assert content =~ "@uncached"
end
test "a Misskey MFM note drops oversized source content instead of parsing it" do
user = insert(:user, ap_id: "https://misskey.example/users/oversized")
note = %{
"id" => "https://misskey.example/notes/4",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "<span class=\"mfm-spin\">safe fallback</span>",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content == "<span class=\"mfm-spin\">safe fallback</span>"
refute Map.has_key?(source, "content")
end
test "a note drops oversized non-MFM source content" do
user = insert(:user, ap_id: "https://example.com/users/source")
note = %{
"id" => "https://example.com/notes/1",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "regular content",
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/markdown"
}
}
%{valid?: true, changes: %{source: source}} = ArticleNotePageValidator.cast_and_validate(note)
assert source == %{"mediaType" => "text/markdown"}
end
test "a Misskey MFM note with legacy _misskey_content is rendered" do
user = insert(:user, ap_id: "https://misskey.example/users/legacy")
note = %{
"id" => "https://misskey.example/notes/5",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" => "original content",
"context" => Utils.generate_context_id(),
"_misskey_content" => "$[spin legacy]"
}
%{valid?: true, changes: %{content: content, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert source == %{"content" => "$[spin legacy]", "mediaType" => "text/x.misskeymarkdown"}
assert content =~ ~s(class="mfm-spin")
assert content =~ "legacy"
end
test "a Misskey MFM note with htmlMfm is scrubbed but not rendered from source content" do
user = insert(:user, ap_id: "https://misskey.example/users/bob")
note = %{
"id" => "https://misskey.example/notes/2",
"type" => "Note",
"actor" => user.ap_id,
"attributedTo" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"content" =>
"<span class=\"mfm-spin\">already rendered</span><script>alert('xss')</script>",
"htmlMfm" => true,
"context" => Utils.generate_context_id(),
"source" => %{
"content" => String.duplicate("x", 5_001),
"mediaType" => "text/x.misskeymarkdown"
}
}
%{valid?: true, changes: %{content: content, htmlMfm: true, source: source}} =
ArticleNotePageValidator.cast_and_validate(note)
assert content == "<span class=\"mfm-spin\">already rendered</span>alert(&#39;xss&#39;)"
refute Map.has_key?(source, "content")
end
test "a Note with validated likes collection validates" do test "a Note with validated likes collection validates" do
insert(:user, ap_id: "https://pol.social/users/mkljczk") insert(:user, ap_id: "https://pol.social/users/mkljczk")

View file

@ -29,7 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
assert {:ok, _update, []} = ObjectValidator.validate(valid_update, []) assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
end end
test "returns an error if the object can't be updated by the actor", %{ test "returns an error if the object can't be updated by the actor (different domain)", %{
valid_update: valid_update valid_update: valid_update
} do } do
other_user = insert(:user, local: false) other_user = insert(:user, local: false)
@ -41,27 +41,72 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
assert {:error, _cng} = ObjectValidator.validate(update, []) assert {:error, _cng} = ObjectValidator.validate(update, [])
end end
test "validates as long as the object is same-origin with the actor", %{ test "returns an error if the object can't be updated by the actor (same domain)", %{
user: user,
valid_update: valid_update valid_update: valid_update
} do } do
other_user = insert(:user) user_ap_id = user.ap_id
user_domain = URI.parse(user_ap_id).host
other_user = insert(:user, local: false, domain: user_domain)
update = update =
valid_update valid_update
|> Map.put("actor", other_user.ap_id) |> Map.put("actor", other_user.ap_id)
assert {:ok, _update, []} = ObjectValidator.validate(update, []) assert {:error, _cng} = ObjectValidator.validate(update, [])
end end
test "validates if the object is not of an Actor type" do test "validates if the object is not of an Actor type", %{user: user} do
note = insert(:note) note = insert(:note, user: user)
updated_note = note.data |> Map.put("content", "edited content") updated_note = note.data |> Map.put("content", "edited content")
other_user = insert(:user)
{:ok, update, _} = Builder.update(other_user, updated_note) {:ok, update, _} = Builder.update(user, updated_note)
assert {:ok, _update, _} = ObjectValidator.validate(update, []) assert {:ok, _update, _} = ObjectValidator.validate(update, [])
end end
test "returns an error if the remote update target is unknown" do
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
update = %{
"type" => "Update",
"actor" => remote_user.ap_id,
"id" => "https://example.com/activities/update-unknown-object",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://example.com/objects/unknown",
"actor" => remote_user.ap_id,
"content" => "edited content",
"published" => "2024-07-25T13:33:31Z",
"updated" => "2024-07-25T13:34:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
refute cng.valid?
assert Keyword.has_key?(cng.errors, :object)
end
test "returns an error if the remote update target IRI is unknown" do
remote_user = insert(:user, local: false, ap_id: "https://example.com/users/alice")
update = %{
"type" => "Update",
"actor" => remote_user.ap_id,
"id" => "https://example.com/activities/update-unknown-object-iri",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => "https://example.com/objects/unknown-iri"
}
assert {:error, %Ecto.Changeset{} = cng} = ObjectValidator.validate(update, local: false)
refute cng.valid?
assert Keyword.has_key?(cng.errors, :object)
end
end end
describe "update note" do describe "update note" do

View file

@ -180,7 +180,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld", "http://localhost:4001/schemas/litepub-0.1.jsonld",
%{ %{
"@language" => "und" "@language" => "und",
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
} }
] ]
} }
@ -192,7 +193,8 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld", "http://localhost:4001/schemas/litepub-0.1.jsonld",
%{ %{
"@language" => "pl" "@language" => "pl",
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
} }
] ]
} }

View file

@ -709,6 +709,47 @@ defmodule Pleroma.Web.CommonAPITest do
assert object.data["source"]["content"] == post assert object.data["source"]["content"] == post
end end
test "it renders MFM posts and marks their ActivityPub representation" do
user = insert(:user)
post = "<p class='scrub-this'>$[spin.speed=1s 13:37]</p>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/x.misskeymarkdown"
})
object = Object.normalize(activity, fetch: false)
assert object.data["htmlMfm"] == true
assert object.data["source"] == %{
"content" => post,
"mediaType" => "text/x.misskeymarkdown"
}
assert object.data["content"] =~ ~s(class="mfm-spin")
assert object.data["content"] =~ ~s(data-mfm-speed="1s")
assert object.data["content"] =~ "13:37"
refute object.data["content"] =~ "scrub-this"
end
test "it falls back safely for malformed MFM" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "$[spin.speed=1s=boom malformed]",
content_type: "text/x.misskeymarkdown"
})
object = Object.normalize(activity, fetch: false)
refute object.data["content"] =~ ~s(class="mfm-spin")
assert object.data["content"] =~ "malformed"
end
test "it does not allow replies to direct messages that are not direct messages themselves" do test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user) user = insert(:user)

View file

@ -180,4 +180,179 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
assert result[:pleroma][:non_anonymous] == true assert result[:pleroma][:non_anonymous] == true
end end
test "prefers votersCount over voters list when both are present" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
assert object.data["votersCount"] == 1
assert length(object.data["voters"]) == 1
object = %{
object
| data: Map.put(object.data, "votersCount", 42)
}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 42
end
test "falls back to voters list when votersCount is absent" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
assert length(object.data["voters"]) == 1
data = Map.delete(object.data, "votersCount")
object = %{object | data: data}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 1
end
test "returns 0 when both votersCount and voters are absent" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
data =
object.data
|> Map.delete("votersCount")
|> Map.delete("voters")
object = %{object | data: data}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 0
end
test "returns 0 when voters list is empty" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Which flavor?",
poll: %{options: ["chocolate", "vanilla"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
data =
object.data
|> Map.delete("votersCount")
|> Map.put("voters", [])
object = %{object | data: data}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 0
end
test "does not inflate votersCount when same voter picks multiple options" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick several",
poll: %{options: ["a", "b", "c"], expires_in: 20, multiple: true}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0, 2])
assert object.data["votersCount"] == 1
assert length(object.data["voters"]) == 1
end
test "preserves votersCount from remote source when existing voter picks another option" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick several",
poll: %{options: ["a", "b"], expires_in: 20, multiple: true}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0, 1])
object = %{object | data: Map.put(object.data, "votersCount", 14)}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 14
end
test "returns 0 when votersCount is explicitly 0" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick one",
poll: %{options: ["a", "b"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
object = %{object | data: Map.put(object.data, "votersCount", 0)}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == 0
end
test "falls back to voters list when votersCount is nil" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Pick one",
poll: %{options: ["a", "b"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
voter = insert(:user)
{:ok, _, object} = CommonAPI.vote(object, voter, [0])
object = %{object | data: Map.put(object.data, "votersCount", nil)}
result = PollView.render("show.json", %{object: object})
assert result[:voters_count] == length(object.data["voters"])
end
end end

View file

@ -47,13 +47,27 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
assert %{valid_signature: false} == conn.assigns assert %{valid_signature: false} == conn.assigns
end end
@tag skip: "known breakage; the testsuite presently depends on it"
test "it considers a mapped identity to be invalid when the identity cannot be found" do test "it considers a mapped identity to be invalid when the identity cannot be found" do
actor = "http://niu.moe/users/rye"
conn = conn =
build_conn(:post, "/doesntmattter", %{"actor" => "http://mastodon.example.org/users/admin"}) build_conn(:post, "/doesntmattter", %{"actor" => actor})
|> set_signature("http://niu.moe/users/rye") |> set_signature(actor)
|> MappedSignatureToIdentityPlug.call(%{}) |> MappedSignatureToIdentityPlug.call(%{})
assert %{valid_signature: false} == conn.assigns assert conn.assigns.valid_signature == false
refute Map.has_key?(conn.assigns, :user)
end
test "it considers a mapped identity to be invalid when embedded actor identity cannot be found" do
actor = "http://niu.moe/users/rye"
conn =
build_conn(:post, "/doesntmattter", %{"actor" => %{"id" => actor}})
|> set_signature(actor)
|> MappedSignatureToIdentityPlug.call(%{})
assert conn.assigns.valid_signature == false
refute Map.has_key?(conn.assigns, :user)
end end
end end

View file

@ -11,9 +11,27 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
alias Pleroma.Workers.ReceiverWorker alias Pleroma.Workers.ReceiverWorker
defp signature_headers_for(%User{} = signer) do
[
{"host", "local.test"},
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
{"digest", "SHA-256=fake-digest"},
{"content-type", "application/activity+json"},
{
"signature",
"keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
}
]
end
defp perform_incoming(params) do
ReceiverWorker.perform(%Oban.Job{
args: %{"op" => "incoming_ap_doc", "params" => params}
})
end
test "it does not retry MRF reject" do test "it does not retry MRF reject" do
params = insert(:note).data params = insert(:note).data
@ -81,16 +99,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/bart") |> Map.put("actor", "https://springfield.social/users/bart")
{:ok, oban_job} = assert {:cancel, {:error, :forbidden}} = perform_incoming(params)
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:error, :forbidden}} = ReceiverWorker.perform(oban_job)
end end
test "when request returns a 404" do test "when request returns a 404" do
@ -98,16 +107,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/troymcclure") |> Map.put("actor", "https://springfield.social/users/troymcclure")
{:ok, oban_job} = assert {:cancel, {:error, :not_found}} = perform_incoming(params)
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
end end
test "when request returns a 410" do test "when request returns a 410" do
@ -115,16 +115,7 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
insert(:note_activity).data insert(:note_activity).data
|> Map.put("actor", "https://springfield.social/users/hankscorpio") |> Map.put("actor", "https://springfield.social/users/hankscorpio")
{:ok, oban_job} = assert {:cancel, {:error, :not_found}} = perform_incoming(params)
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:error, :not_found}} = ReceiverWorker.perform(oban_job)
end end
test "when user account is disabled" do test "when user account is disabled" do
@ -138,86 +129,16 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
{:ok, %User{}} = User.set_activation(user, false) {:ok, %User{}} = User.set_activation(user, false)
{:ok, oban_job} = assert {:cancel, {:user_active, false}} = perform_incoming(params)
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, {:user_active, false}} = ReceiverWorker.perform(oban_job)
end end
end end
test "it can validate the signature" do
Tesla.Mock.mock(fn
%{url: "https://phpc.social/users/denniskoch"} ->
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/denniskoch.json"),
headers: [{"content-type", "application/activity+json"}]
}
%{url: "https://phpc.social/users/denniskoch/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "phpc.social")
|> String.replace("{{nickname}}", "denniskoch")
}
end)
params =
File.read!("test/fixtures/receiver_worker_signature_activity.json") |> Jason.decode!()
req_headers = [
["accept-encoding", "gzip"],
["content-length", "5184"],
["content-type", "application/activity+json"],
["date", "Thu, 25 Jul 2024 13:33:31 GMT"],
["digest", "SHA-256=ouge/6HP2/QryG6F3JNtZ6vzs/hSwMk67xdxe87eH7A="],
["host", "bikeshed.party"],
[
"signature",
"keyId=\"https://mastodon.social/users/bastianallgeier#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"ymE3vn5Iw50N6ukSp8oIuXJB5SBjGAGjBasdTDvn+ahZIzq2SIJfmVCsIIzyqIROnhWyQoTbavTclVojEqdaeOx+Ejz2wBnRBmhz5oemJLk4RnnCH0lwMWyzeY98YAvxi9Rq57Gojuv/1lBqyGa+rDzynyJpAMyFk17XIZpjMKuTNMCbjMDy76ILHqArykAIL/v1zxkgwxY/+ELzxqMpNqtZ+kQ29znNMUBB3eVZ/mNAHAz6o33Y9VKxM2jw+08vtuIZOusXyiHbRiaj2g5HtN2WBUw1MzzfRfHF2/yy7rcipobeoyk5RvP5SyHV3WrIeZ3iyoNfmv33y8fxllF0EA==\""
],
[
"user-agent",
"http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-07-25; +https://mastodon.social/)"
]
]
{:ok, oban_job} =
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: req_headers,
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:ok, %Pleroma.Activity{}} = ReceiverWorker.perform(oban_job)
end
test "cancels due to origin containment" do test "cancels due to origin containment" do
params = params =
insert(:note_activity).data insert(:note_activity).data
|> Map.put("id", "https://notorigindomain.com/activity") |> Map.put("id", "https://notorigindomain.com/activity")
{:ok, oban_job} = assert {:cancel, :origin_containment_failed} = perform_incoming(params)
Federator.incoming_ap_doc(%{
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, :origin_containment_failed} = ReceiverWorker.perform(oban_job)
end end
test "canceled due to deleted object" do test "canceled due to deleted object" do
@ -233,16 +154,114 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do
} }
end) end)
{:ok, oban_job} = assert {:cancel, _} = perform_incoming(params)
Federator.incoming_ap_doc(%{ end
method: "POST",
req_headers: [],
request_path: "/inbox",
params: params,
query_string: ""
})
assert {:cancel, _} = ReceiverWorker.perform(oban_job) test "delegates legacy failed-signature metadata jobs instead of processing them as trusted" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/legacy-forged-note"
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/legacy-forged-create",
"context" => "https://two.com/contexts/legacy-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/legacy-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert {:cancel, :actor_signature_mismatch} =
ReceiverWorker.perform(%Oban.Job{
args: %{
"op" => "incoming_ap_doc",
"method" => "POST",
"params" => create,
"req_headers" => signature_headers_for(alice),
"request_path" => "/inbox",
"query_string" => ""
}
})
refute Pleroma.Activity.get_by_ap_id(create["id"])
refute Pleroma.Object.get_by_ap_id(object_id)
end
test "fails closed for the old persisted failed-signature job shape" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/old-shape-forged-note"
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/old-shape-forged-create",
"context" => "https://two.com/contexts/old-shape-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/old-shape-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert {:cancel, :missing_signature_retry_metadata} =
ReceiverWorker.perform(%Oban.Job{
args: %{
"op" => "incoming_ap_doc",
"params" => create,
"req_headers" => signature_headers_for(alice),
"timeout" => 20_000
}
})
refute Pleroma.Activity.get_by_ap_id(create["id"])
refute Pleroma.Object.get_by_ap_id(object_id)
end
test "fails closed for legacy retry jobs missing one metadata field" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params = insert(:note_activity).data
assert {:cancel, :missing_signature_retry_metadata} =
ReceiverWorker.perform(%Oban.Job{
args: %{
"op" => "incoming_ap_doc",
"method" => "POST",
"params" => params,
"req_headers" => signature_headers_for(alice),
"request_path" => "/inbox"
}
})
end
test "fails closed for malformed legacy metadata jobs without params" do
assert {:cancel, :missing_signature_retry_metadata} =
ReceiverWorker.perform(%Oban.Job{
args: %{
"op" => "incoming_ap_doc",
"req_headers" => [],
"timeout" => 20_000
}
})
end end
describe "Server reachability:" do describe "Server reachability:" do

View file

@ -0,0 +1,574 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.SignatureRetryWorkerTest do
use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import ExUnit.CaptureLog
import Pleroma.Factory
@moduletag capture_log: true
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Signature
alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.Federator
alias Pleroma.Workers.SignatureRetryWorker
defp signature_headers_for(%User{} = signer) do
[
{"host", "local.test"},
{"date", "Thu, 25 Jul 2024 13:33:31 GMT"},
{"digest", "SHA-256=fake-digest"},
{"content-type", "application/activity+json"},
{
"signature",
"keyId=\"#{signer.ap_id}#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"fake-signature\""
}
]
end
defp stub_actor_fetch(%User{} = signer) do
signer_json = UserView.render("user.json", %{user: signer}) |> Map.delete("featured")
Tesla.Mock.mock(fn
%{url: url} when url == signer.ap_id ->
%Tesla.Env{
status: 200,
body: Jason.encode!(signer_json),
headers: HttpRequestMock.activitypub_object_headers()
}
end)
end
defp expect_signature_from(%User{} = signer) do
stub_actor_fetch(signer)
Mox.expect(Pleroma.StubbedHTTPSignaturesMock, :validate_conn, fn _conn -> true end)
end
defp enqueue_failed_signature(params, signer) do
Federator.incoming_failed_signature_ap_doc(%{
method: "POST",
req_headers: signature_headers_for(signer),
request_path: "/inbox",
params: params,
query_string: ""
})
end
defp failed_signature_job(params, req_headers, opts \\ []) do
%Oban.Job{
args: %{
"op" => "incoming_failed_signature_ap_doc",
"method" => Keyword.get(opts, :method, "POST"),
"req_headers" => req_headers,
"request_path" => Keyword.get(opts, :request_path, "/inbox"),
"params" => params,
"query_string" => Keyword.get(opts, :query_string, "")
}
}
end
defp assert_mismatched_signature_cancelled(params, signer) do
assert {:ok, oban_job} = enqueue_failed_signature(params, signer)
capture_log([level: :warning], fn ->
assert {:cancel, :actor_signature_mismatch} = SignatureRetryWorker.perform(oban_job)
end)
end
test "Federator preserves request metadata for failed-signature retry jobs" do
params = insert(:note_activity).data
req_headers = [
{"host", "local.test"},
{"signature", "keyId=\"https://one.com/users/alice#main-key\""}
]
assert {:ok, oban_job} =
Federator.incoming_failed_signature_ap_doc(%{
method: "POST",
req_headers: req_headers,
request_path: "/inbox",
params: params,
query_string: "foo=bar"
})
assert oban_job.worker == "Pleroma.Workers.SignatureRetryWorker"
assert %{
"op" => "incoming_failed_signature_ap_doc",
"method" => "POST",
"req_headers" => ^req_headers,
"request_path" => "/inbox",
"params" => ^params,
"query_string" => "foo=bar"
} = oban_job.args
end
test "cancels retry jobs without request metadata" do
params = insert(:note_activity).data
log =
capture_log([level: :warning], fn ->
assert {:cancel, :missing_signature_retry_metadata} =
SignatureRetryWorker.perform(%Oban.Job{
args: %{"op" => "incoming_failed_signature_ap_doc", "params" => params}
})
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:missing_signature_retry_metadata"
assert log =~ "payload_actor=#{inspect(params["actor"])}"
assert log =~ "activity_id=#{inspect(params["id"])}"
assert log =~ "type=#{inspect(params["type"])}"
assert log =~ "request_path=nil"
end
test "cancels retry jobs with malformed serialized request headers" do
params = insert(:note_activity).data
log =
capture_log([level: :warning], fn ->
assert {:cancel, :invalid_signature_retry_metadata} =
SignatureRetryWorker.perform(failed_signature_job(params, [["signature"]]))
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:invalid_signature_retry_metadata"
assert log =~ "signature_actor=nil"
assert log =~ "request_path=\"/inbox\""
end
test "cancels retry jobs without a signature header" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params = insert(:note_activity, user: alice).data
log =
capture_log([level: :warning], fn ->
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(
failed_signature_job(params, [{"host", "local.test"}])
)
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:invalid_signature"
assert log =~ "payload_actor=#{inspect(params["actor"])}"
assert log =~ "signature_actor=nil"
assert log =~ "request_path=\"/inbox\""
end
test "cancels missing signature before fetching an unavailable payload actor" do
params =
insert(:note_activity).data
|> Map.put("actor", "https://unavailable.example/users/bob")
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(failed_signature_job(params, [{"host", "local.test"}]))
end
test "cancels signer mismatch before fetching an unavailable payload actor" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params =
insert(:note_activity).data
|> Map.put("actor", "https://unavailable.example/users/bob")
assert {:cancel, :actor_signature_mismatch} =
SignatureRetryWorker.perform(
failed_signature_job(params, signature_headers_for(alice))
)
end
test "cancels retry jobs with a signature header without keyId" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params = insert(:note_activity, user: alice).data
req_headers = [{"signature", "algorithm=\"rsa-sha256\",signature=\"fake-signature\""}]
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
end
test "cancels retry jobs with an unparsable signature keyId" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
params = insert(:note_activity, user: alice).data
req_headers = [{"signature", "keyId=\"not an activitypub id\",signature=\"fake-signature\""}]
assert {:cancel, :invalid_signature} =
SignatureRetryWorker.perform(failed_signature_job(params, req_headers))
end
test "cancels when the refetched key still cannot validate the signature" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
create = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/invalid-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://one.com/objects/invalid-signature-note",
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
stub_actor_fetch(alice)
assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
log =
capture_log([level: :warning], fn ->
assert {:cancel, :invalid_signature} = SignatureRetryWorker.perform(oban_job)
end)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:invalid_signature"
assert log =~ "payload_actor=\"https://one.com/users/alice\""
assert log =~ "signature_actor=\"https://one.com/users/alice\""
assert log =~ "activity_id=\"https://one.com/activities/invalid-signature-create\""
assert log =~ "type=\"Create\""
assert log =~ "request_path=\"/inbox\""
refute Activity.get_by_ap_id(create["id"])
end
test "processes the activity after refetching a valid matching signature" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
create = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/valid-signature-create",
"context" => "https://one.com/contexts/valid-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://one.com/objects/valid-signature-note",
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"context" => "https://one.com/contexts/valid-signature-create",
"content" => "valid post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
expect_signature_from(alice)
assert {:ok, oban_job} = enqueue_failed_signature(create, alice)
assert {:ok, %Activity{}} = SignatureRetryWorker.perform(oban_job)
assert Activity.get_by_ap_id(create["id"])
end
test "processes the activity when a real signature validates with a query string" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
create = %{
"type" => "Create",
"actor" => alice.ap_id,
"id" => "https://one.com/activities/valid-query-signature-create",
"context" => "https://one.com/contexts/valid-query-signature-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://one.com/objects/valid-query-signature-note",
"actor" => alice.ap_id,
"attributedTo" => alice.ap_id,
"context" => "https://one.com/contexts/valid-query-signature-create",
"content" => "valid signed post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
stub_actor_fetch(alice)
date = "Thu, 25 Jul 2024 13:33:31 GMT"
digest = "SHA-256=fake-digest"
signature =
Signature.sign(alice, %{
"(request-target)" => "post /inbox?foo=bar",
"content-type" => "application/activity+json",
date: date,
digest: digest,
host: "local.test"
})
req_headers = [
["host", "local.test"],
["date", date],
["digest", digest],
["content-type", "application/activity+json"],
["signature", signature]
]
assert {:ok, %Activity{}} =
SignatureRetryWorker.perform(
failed_signature_job(create, req_headers, query_string: "foo=bar")
)
assert Activity.get_by_ap_id(create["id"])
end
test "cancels when signature actor does not match payload actor" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note =
insert(:note,
user: bob,
object_local: false,
data: %{"id" => "https://two.com/objects/malicious-update-note"}
)
update = %{
"type" => "Update",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/malicious-update",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data
}
assert_mismatched_signature_cancelled(update, alice)
end
test "cancels signature actor mismatch through Federator-created jobs" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note =
insert(:note,
user: bob,
object_local: false,
data: %{"id" => "https://two.com/objects/federator-malicious-note"}
)
update = %{
"type" => "Update",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/federator-malicious-update",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data
}
assert_mismatched_signature_cancelled(update, alice)
end
test "cancels signature actor mismatch before processing a forged Create" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://two.com/objects/forged-note",
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert_mismatched_signature_cancelled(create, alice)
end
test "cancels signature actor mismatch when payload actor is embedded" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
create = %{
"type" => "Create",
"actor" => %{"id" => bob.ap_id},
"id" => "https://two.com/activities/embedded-actor-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://two.com/objects/embedded-actor-forged-note",
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert_mismatched_signature_cancelled(create, alice)
end
test "logs signature actor mismatch retry rejections" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/logged-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "https://two.com/objects/logged-forged-note",
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
log = assert_mismatched_signature_cancelled(create, alice)
assert log =~ "Failed-signature inbox retry rejected"
assert log =~ "reason=:actor_signature_mismatch"
assert log =~ "payload_actor=\"https://two.com/users/bob\""
assert log =~ "signature_actor=\"https://one.com/users/alice\""
assert log =~ "activity_id=\"https://two.com/activities/logged-forged-create\""
assert log =~ "type=\"Create\""
assert log =~ "request_path=\"/inbox\""
end
test "cancels signature actor mismatch before actually creating a forged post" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
object_id = "https://two.com/objects/actually-forged-note"
create = %{
"type" => "Create",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/actually-forged-create",
"context" => "https://two.com/contexts/actually-forged-create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => object_id,
"actor" => bob.ap_id,
"attributedTo" => bob.ap_id,
"context" => "https://two.com/contexts/actually-forged-create",
"content" => "forged post",
"published" => "2024-07-25T13:33:31Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => []
}
}
assert_mismatched_signature_cancelled(create, alice)
refute Object.get_by_ap_id(object_id)
end
test "cancels signature actor mismatch before processing a forged Like" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
like = %{
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
assert_mismatched_signature_cancelled(like, alice)
end
test "cancels signature actor mismatch before actually creating a forged Like" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
like = %{
"type" => "Like",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/actually-forged-like",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
assert_mismatched_signature_cancelled(like, alice)
refute Activity.get_by_ap_id(like["id"])
end
test "cancels signature actor mismatch before processing a forged Announce" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
note = insert(:note)
announce = %{
"type" => "Announce",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-announce",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => note.data["id"]
}
assert_mismatched_signature_cancelled(announce, alice)
end
test "cancels signature actor mismatch before processing a forged Follow" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
followed = insert(:user)
follow = %{
"type" => "Follow",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-follow",
"to" => [followed.ap_id],
"cc" => [],
"object" => followed.ap_id
}
assert_mismatched_signature_cancelled(follow, alice)
end
test "cancels signature actor mismatch before processing a forged Undo" do
alice = insert(:user, local: false, ap_id: "https://one.com/users/alice")
bob = insert(:user, local: false, ap_id: "https://two.com/users/bob")
undo = %{
"type" => "Undo",
"actor" => bob.ap_id,
"id" => "https://two.com/activities/forged-undo",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => "https://two.com/activities/existing-bob-activity"
}
assert_mismatched_signature_cancelled(undo, alice)
end
end