├── .builds
├── images.yml
└── main.yml
├── .containerignore
├── .env.dist
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── activitypub
├── activitypub.go
└── activitypub_test.go
├── app.go
├── app_test.go
├── cmd
├── control
│ └── main.go
└── fedbox
│ └── main.go
├── doc
├── INSTALL.md
├── c2s.md
└── todo.md
├── go.mod
├── handlers.go
├── handlers_test.go
├── httpsig.go
├── images
├── .env.default
├── .gitignore
├── Makefile
├── README.md
├── build.sh
├── gen-certs.sh
└── image.sh
├── integration
├── build.go
├── c2s_test.go
├── go.mod
├── main_test.go
├── mocks
│ ├── .env
│ └── import.json
└── setup.go
├── internal
├── assets
│ ├── assets.go
│ ├── assets_prod.go
│ └── templates
│ │ ├── error.html
│ │ ├── login.html
│ │ └── password.html
├── cmd
│ ├── accounts.go
│ ├── activitypub.go
│ ├── bootstrap.go
│ ├── control.go
│ ├── fedbox.go
│ ├── filters.go
│ ├── fix-storage-collections.go
│ ├── maintenance.go
│ ├── oauth.go
│ └── output.go
├── config
│ ├── config.go
│ ├── config_test.go
│ ├── storage_all.go
│ ├── storage_badger.go
│ ├── storage_boltdb.go
│ ├── storage_fs.go
│ └── storage_sqlite.go
├── env
│ ├── env.go
│ └── env_test.go
├── storage
│ ├── all.go
│ ├── badger.go
│ ├── boltdb.go
│ ├── fs.go
│ └── sqlite.go
└── utils.go
├── metatada.go
├── middleware.go
├── middlewares_test.go
├── oauth.go
├── routes.go
├── routes_test.go
├── storage
└── storage.go
├── storage_all.go
├── storage_badger.go
├── storage_boltdb.go
├── storage_fs.go
├── storage_sqlite.go
├── systemd
├── fedbox.service.in
└── fedbox.socket.in
├── tests
├── .cache
│ └── .gitkeep
├── .gitignore
├── Makefile
├── c2s_test.go
├── common.go
├── main_test.go
├── mockapp.go
├── mocks
│ ├── c2s
│ │ ├── activities
│ │ │ ├── activity-private.json
│ │ │ ├── activity.json
│ │ │ ├── block-actor.json
│ │ │ ├── create-1.json
│ │ │ ├── create-2.json
│ │ │ ├── create-actor.json
│ │ │ ├── create-article.json
│ │ │ ├── create-object-with-federated-cc.json
│ │ │ ├── create-object.json
│ │ │ ├── question-with-anyOf.json
│ │ │ ├── question-with-oneOf.json
│ │ │ ├── question.json
│ │ │ └── update-actor.json
│ │ ├── actors
│ │ │ ├── actor-admin.json
│ │ │ ├── actor-element_a.json
│ │ │ ├── actor-element_b.json
│ │ │ ├── actor-element_c.json
│ │ │ ├── actor-element_d.json
│ │ │ ├── actor-element_e.json
│ │ │ ├── actor-element_f.json
│ │ │ ├── actor-element_g.json
│ │ │ ├── actor-element_h.json
│ │ │ ├── actor-element_i.json
│ │ │ ├── actor-extra.json
│ │ │ ├── actor-johndoe.json
│ │ │ ├── application-11.json
│ │ │ ├── application-12.json
│ │ │ ├── application-13.json
│ │ │ ├── application-14.json
│ │ │ ├── application-15.json
│ │ │ ├── application.json
│ │ │ ├── group-16.json
│ │ │ ├── group-17.json
│ │ │ ├── group-18.json
│ │ │ ├── group-19.json
│ │ │ ├── group-20.json
│ │ │ └── service.json
│ │ └── objects
│ │ │ ├── note-1.json
│ │ │ ├── note-replyTo-1-2-5.json
│ │ │ ├── note-replyTo-1-and-2.json
│ │ │ ├── note.json
│ │ │ ├── page-2.json
│ │ │ ├── place-4.json
│ │ │ ├── tag-mod.json
│ │ │ └── tombstone-3.json
│ ├── keys
│ │ ├── ed25519.prv
│ │ ├── ed25519.pub
│ │ ├── rsa2048.prv
│ │ └── rsa2048.pub
│ └── s2s
│ │ ├── accept-follow.json
│ │ ├── activities
│ │ ├── accept-follow-666-johndoe.json
│ │ ├── create-actor-666.json
│ │ ├── create-note-1.json
│ │ ├── follow-666-johndoe.json
│ │ └── follow-mitra.json
│ │ ├── actors
│ │ ├── actor-666.json
│ │ └── mitra-user.json
│ │ ├── create-object.json
│ │ └── objects
│ │ └── note-1.json
├── s2s_test.go
└── script.js
└── tools
├── bootstrap.sh
├── build-images.sh
├── clientadd.sh
├── run-container
├── run-tests.sh
├── update.sh
└── useradd.sh
/.builds/images.yml:
--------------------------------------------------------------------------------
1 | image: archlinux
2 | secrets:
3 | - 3f30fd61-e33d-4198-aafb-0ff341e9db1c
4 | packages:
5 | - podman
6 | - buildah
7 | - passt
8 | sources:
9 | - https://git.sr.ht/~mariusor/fedbox
10 | tasks:
11 | - images: |
12 | test ${BUILD_SUBMITTER} != "git.sr.ht" && complete-build
13 | set -a +x
14 | source ~/.buildah.env
15 |
16 | _user=$(id -un)
17 |
18 | echo 'unqualified-search-registries = ["docker.io"]' | sudo tee /etc/containers/registries.conf.d/unq-search.conf
19 | echo "${_user}:10000:65536" | sudo tee /etc/subuid
20 | echo "${_user}:10000:65536" | sudo tee /etc/subgid
21 | podman system migrate
22 |
23 | podman login -u="${BUILDAH_USER}" -p="${BUILDAH_SECRET}" quay.io
24 |
25 | set --
26 | cd fedbox || exit
27 |
28 | _sha=$(git rev-parse --short HEAD)
29 | _branch=$(git branch --points-at=${_sha} | tail -n1 | tr -d '* ')
30 | _version=$(printf "%s-%s" "${_branch}" "${_sha}")
31 |
32 | make -C images cert builder
33 | _push() {
34 | _storage=${1:-all}
35 | make -C images STORAGE="${_storage}" ENV=dev VERSION="${_version}" push
36 |
37 | if [ "${_branch}" = "master" ]; then
38 | make -C images STORAGE="${_storage}" ENV=qa VERSION="${_version}" push
39 | fi
40 |
41 | _tag=$(git describe --long --tags || true)
42 | if [ -n "${_tag}" ]; then
43 | make -C images STORAGE="${_storage}" ENV=prod VERSION="${_tag}" push
44 | fi
45 | }
46 | #_push
47 | _push fs
48 | _push sqlite
49 | _push boltdb
50 | # I guess I don't need everything while we're in heavy development
51 | complete-build
52 | _push badger
53 |
--------------------------------------------------------------------------------
/.builds/main.yml:
--------------------------------------------------------------------------------
1 | image: archlinux
2 | packages:
3 | - go
4 | sources:
5 | - https://github.com/go-ap/fedbox
6 | environment:
7 | GO111MODULE: 'on'
8 | secrets:
9 | - 3dcea276-38d6-4a7e-85e5-20cbc903e1ea
10 | tasks:
11 | - setup: |
12 | test ${BUILD_SUBMITTER} != "git.sr.ht" && complete-build
13 | cd fedbox && make download && go mod vendor
14 | - build: |
15 | cd fedbox
16 | make STORAGE=fs clean all
17 | make STORAGE=boltdb clean all
18 | make STORAGE=sqlite clean all
19 | make STORAGE=all clean all
20 | - tests: |
21 | cd fedbox
22 | make test
23 | - push_to_github: |
24 | test ${BUILD_SUBMITTER} != "git.sr.ht" && complete-build
25 | set -a +x
26 | ssh-keyscan -H github.com >> ~/.ssh/known_hosts
27 |
28 | cd fedbox
29 | git remote add hub git@github.com:go-ap/fedbox
30 | git push hub --force --all
31 | - coverage: |
32 | set -a +x
33 | cd fedbox
34 | make coverage
35 | - integration-fs: |
36 | cd fedbox
37 | ./tools/run-tests.sh fs
38 | - integration-boltdb: |
39 | cd fedbox
40 | ./tools/run-tests.sh boltdb
41 | - integration-sqlite: |
42 | cd fedbox
43 | ./tools/run-tests.sh sqlite
44 | complete-build
45 | - integration-badger: |
46 | cd fedbox
47 | ./tools/run-tests.sh badger
48 |
--------------------------------------------------------------------------------
/.containerignore:
--------------------------------------------------------------------------------
1 | **/.builds
2 | **/.git
3 | **/.idea
4 | **/.run
5 | **/bin
6 | **/tests
7 | **/tools
8 |
--------------------------------------------------------------------------------
/.env.dist:
--------------------------------------------------------------------------------
1 | # The environment for current run, valid values are: test, dev, qa, prod
2 | FEDBOX_ENV=dev
3 |
4 | # The default hostname for the current instance
5 | FEDBOX_HOSTNAME=fedbox.local
6 |
7 | # The connection string to listen on:
8 | # It can be a host/IP + port pair: "127.6.6.6:7666"
9 | # It can be a path on disk, which will be used to start a unix domain socket: "/var/run/fedbox-local.sock"
10 | # It can be the magic string "systemd" to be used for systemd socket activation.
11 | FEDBOX_LISTEN=localhost:4000
12 |
13 | # The storage type to use, valid values:
14 | # - fs: store objects in plain json files, using symlinking for items that belong to multiple collections
15 | # - boltdb: use boltdb
16 | # - badger: use badger
17 | # - sqlite: use sqlite
18 | FEDBOX_STORAGE=fs
19 |
20 | # The base path for the storage backend
21 | # It supports some conveniences to compose the path:
22 | # If a path starts with '~', it gets replaced with the current user's HOME directory,
23 | # if HOME variable is present in the running environment.
24 | # If a path starts contains '%env%' it gets replaced with the current FEDBOX_ENV value.
25 | # If a path starts contains '%storage%' it gets replaced with the current FEDBOX_STORAGE value.
26 | # If a path starts contains '%host%' it gets replaced with the current FEDBOX_HOSTNAME value.
27 | FEDBOX_STORAGE_PATH=.
28 |
29 | # If we should enable TLS for incoming connections, this is a prerequisite of having HTTP2 working
30 | FEDBOX_HTTPS=true
31 |
32 | # The path for the private key used in the TLS connections
33 | FEDBOX_KEY_PATH=fedbox.git.key
34 |
35 | # The path for the TLS certificate used in the connections
36 | FEDBOX_CERT_PATH=fedbox.git.crt
37 |
38 | # Disable cache support for the requests handlers and for the storage backends that support it
39 | FEDBOX_DISABLE_CACHE=false
40 |
41 | # Disable cache support strictly for requests handlers
42 | FEDBOX_DISABLE_STORAGE_CACHE=false
43 |
44 | # Disable cache support strictly for the storage backends that support it
45 | FEDBOX_DISABLE_REQUEST_CACHE=false
46 |
47 | # Disable storage indexing support for the backends that support it
48 | FEDBOX_DISABLE_STORAGE_INDEX=false
49 |
50 | # Disable features that Mastodon servers do not support.
51 | FEDBOX_DISABLE_MASTODON_COMPATIBILITY=false
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .run/
2 | .env
3 | .env.dev
4 | .idea/
5 | *.orig
6 | go.sum
7 | bin/*
8 | *.pem
9 | *.crt
10 | *.key
11 | *.coverprofile
12 | *.bdb
13 | *.tar.*
14 | .cache
15 | internal/assets/assets.gen.go
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Golang ActitvityPub
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := bash
2 | .ONESHELL:
3 | .SHELLFLAGS := -eu -o pipefail -c
4 | .DELETE_ON_ERROR:
5 | MAKEFLAGS += --warn-undefined-variables
6 | MAKEFLAGS += --no-builtin-rules
7 |
8 | FEDBOX_HOSTNAME ?= fedbox.git
9 | STORAGE ?= all
10 | ENV ?= dev
11 | PROJECT ?= fedbox
12 | VERSION ?= HEAD
13 |
14 | LDFLAGS ?= -X main.version=$(VERSION)
15 | BUILDFLAGS ?= -a -ldflags '$(LDFLAGS)'
16 | TEST_FLAGS ?= -count=1
17 |
18 | UPX = upx
19 | M4 = m4
20 | M4_FLAGS =
21 |
22 | DESTDIR ?= /
23 | INSTALL_PREFIX ?= usr/local
24 |
25 | GO ?= go
26 | APPSOURCES := $(wildcard ./*.go activitypub/*.go internal/*/*.go storage/*/*.go)
27 | ASSETFILES := $(wildcard templates/*)
28 |
29 | TAGS := $(ENV) storage_$(STORAGE)
30 |
31 | export CGO_ENABLED=0
32 |
33 | ifeq ($(shell git describe --always > /dev/null 2>&1 ; echo $$?), 0)
34 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD | tr '/' '-')
35 | HASH=$(shell git rev-parse --short HEAD)
36 | VERSION = $(shell printf "%s-%s" "$(BRANCH)" "$(HASH)")
37 | endif
38 | ifeq ($(shell git describe --tags > /dev/null 2>&1 ; echo $$?), 0)
39 | VERSION = $(shell git describe --tags | tr '/' '-')
40 | endif
41 |
42 | ifneq ($(ENV),dev)
43 | LDFLAGS += -s -w -extldflags "-static"
44 | BUILDFLAGS += -trimpath
45 | endif
46 |
47 | BUILD := $(GO) build $(BUILDFLAGS)
48 | TEST := $(GO) test $(BUILDFLAGS)
49 |
50 | .PHONY: all run clean test coverage integration install download help
51 |
52 | .DEFAULT_GOAL := help
53 |
54 | help: ## Help target that shows this message.
55 | @sed -rn 's/^([^:]+):.*[ ]##[ ](.+)/\1:\2/p' $(MAKEFILE_LIST) | column -ts: -l2
56 |
57 | all: fedbox fedboxctl ##
58 |
59 | download: go.sum ## Downloads dependencies and tidies the go.mod file.
60 |
61 | go.sum: go.mod
62 | $(GO) mod download all
63 | $(GO) mod tidy
64 |
65 | fedbox: bin/fedbox ## Builds the main FedBOX service binary.
66 | bin/fedbox: go.mod cmd/fedbox/main.go $(APPSOURCES)
67 | $(BUILD) -tags "$(TAGS)" -o $@ ./cmd/fedbox/main.go
68 | ifneq ($(ENV),dev)
69 | $(UPX) -q --mono --no-progress --best $@ || true
70 | endif
71 |
72 | fedboxctl: bin/fedboxctl ## Builds the control binary for the FedBOX service.
73 | bin/fedboxctl: go.mod cmd/control/main.go $(APPSOURCES)
74 | $(BUILD) -tags "$(TAGS)" -o $@ ./cmd/control/main.go
75 | ifneq ($(ENV),dev)
76 | $(UPX) -q --mono --no-progress --best $@ || true
77 | endif
78 |
79 | systemd/fedbox.service: systemd/fedbox.service.in ## Creates a systemd service file for the FedBOX service.
80 | $(M4) $(M4_FLAGS) -DWORKING_DIR=$(STORAGE_PATH) $< >$@
81 |
82 | systemd/fedbox.socket: systemd/fedbox.socket.in ## Creates a socket systemd unit file to accompany the service file.
83 | $(M4) $(M4_FLAGS) -DLISTEN_HOST=$(LISTEN_HOST) -DLISTEN_PORT=$(LISTEN_PORT) $< >$@
84 |
85 |
86 | run: fedbox ## Runs the FedBOX binary.
87 | @./bin/fedbox
88 |
89 | clean: ## Cleanup the build workspace.
90 | -$(RM) bin/*
91 | $(MAKE) -C tests $@
92 |
93 | test: TEST_TARGET := . ./{activitypub,storage,internal}/...
94 | test: download ## Run unit tests for the service.
95 | $(TEST) $(TEST_FLAGS) -tags "$(TAGS)" $(TEST_TARGET)
96 |
97 | coverage: TEST_TARGET := .
98 | coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT).coverprofile
99 | coverage: test ## Run unit tests for the service with coverage.
100 |
101 | integration: download ## Run integration tests for the service.
102 | $(MAKE) -C tests $@
103 |
104 | $(FEDBOX_HOSTNAME).key $(FEDBOX_HOSTNAME).crt:
105 | openssl req -subj "/C=AQ/ST=Omond/L=Omond/O=*.$(FEDBOX_HOSTNAME)/OU=none/CN=*.$(FEDBOX_HOSTNAME)" -newkey rsa:2048 -sha256 -keyout $(FEDBOX_HOSTNAME).key -nodes -x509 -days 365 -out $(FEDBOX_HOSTNAME).crt
106 |
107 | $(FEDBOX_HOSTNAME).pem: $(FEDBOX_HOSTNAME).key $(FEDBOX_HOSTNAME).crt
108 | cat $(FEDBOX_HOSTNAME).key $(FEDBOX_HOSTNAME).crt > $(FEDBOX_HOSTNAME).pem
109 |
110 | cert: $(FEDBOX_HOSTNAME).key ## Create a certificate.
111 |
112 | install: ./bin/fedbox systemd/fedbox.service systemd/fedbox.socket $(FEDBOX_HOSTNAME).crt $(FEDBOX_HOSTNAME).key ## Install the application.
113 | useradd -m -s /bin/false -u 2000 fedbox
114 | install bin/fedbox $(DESTDIR)$(INSTALL_PREFIX)/bin
115 | install -m 644 -o fedbox systemd/fedbox.service $(DESTDIR)/etc/systemd/system
116 | install -m 644 -o fedbox systemd/fedbox.socket $(DESTDIR)/etc/systemd/system
117 | install -m 600 -o fedbox .env.prod $(STORAGE_PATH)
118 | install -m 600 -o $(FEDBOX_HOSTNAME).crt $(STORAGE_PATH)
119 | install -m 600 -o $(FEDBOX_HOSTNAME).key $(STORAGE_PATH)
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FedBOX
2 |
3 | [](https://raw.githubusercontent.com/go-ap/fedbox/master/LICENSE)
4 | [](https://builds.sr.ht/~mariusor/fedbox)
5 | [](https://goreportcard.com/report/github.com/go-ap/fedbox)
6 |
7 | FedBOX is a simple ActivityPub enabled server. Its goal is to serve as a reference implementation for the rest of the [GoActivityPub](https://github.com/go-ap) packages.
8 |
9 | It provides the base for some of the common functionality that such a service would require, such as: HTTP handlers and middlewares, storage and filtering etc.
10 |
11 | The current iteration can persist data to [BoltDB](https://go.etcd.io/bbolt), [Badger](https://github.com/dgraph-io/badger), [SQLite](https://gitlab.com/cznic/sqlite) and directly on the file system, but I want to also add support for PostgreSQL.
12 |
13 | ## Features
14 |
15 | ### Support for C2S ActivityPub:
16 |
17 | * Support for content management actitivies: `Create`, `Update`, `Delete`.
18 | * `Follow`, `Accept`, `Reject` with actors as objects.
19 | * Appreciation activities: `Like`, `Dislike`.
20 | * Reaction activities: `Block` on actors, `Flag` on objects.
21 | * Negating content management and appreciation activities using `Undo`.
22 | * OAuth2 authentication
23 |
24 | ### Support for S2S ActivityPub
25 |
26 | * Support the same operations as the client to server activities.
27 | * Capabilities of generating and loading HTTP Signatures from requests.
28 |
29 | ## Installation
30 |
31 | See the [INSTALL](./doc/INSTALL.md) file.
32 |
33 | ## Further reading
34 |
35 | If you are interested in using FedBOX from an application developer point of view, make sure to read the [Client to Server](./doc/c2s.md) document, which details how the local flavour of ActivityPub C2S API can be used.
36 |
37 | More information about FedBOX and the other packages in the GoActivityPub library can be found on the [wiki](https://man.sr.ht/~mariusor/go-activitypub/index.md).
38 |
39 | ## Contact and feedback
40 |
41 | If you have problems, questions, ideas or suggestions, please contact us by posting to the [mailing list](https://lists.sr.ht/~mariusor/activitypub-go), or on [GitHub](https://github.com/go-ap/fedbox/issues). If you desire quick feedback, the mailing list is preferred, as the GitHub issues are not checked very often.
42 |
--------------------------------------------------------------------------------
/activitypub/activitypub.go:
--------------------------------------------------------------------------------
1 | package activitypub
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "path"
7 |
8 | vocab "github.com/go-ap/activitypub"
9 | "github.com/go-ap/errors"
10 | "github.com/go-ap/filters"
11 | "github.com/go-ap/processing"
12 | "github.com/pborman/uuid"
13 | )
14 |
15 | const (
16 | developerURL = vocab.IRI("https://github.com/mariusor")
17 | ProjectURL = vocab.IRI("https://github.com/go-ap/fedbox")
18 | )
19 |
20 | func Self(baseURL vocab.IRI) vocab.Service {
21 | u, _ := baseURL.URL()
22 | oauth := *u
23 | oauth.Path = path.Join(oauth.Path, "oauth/")
24 | s := vocab.Service{
25 | ID: baseURL,
26 | Type: vocab.ServiceType,
27 | Name: vocab.NaturalLanguageValuesNew(vocab.DefaultLangRef("self")),
28 | Context: ProjectURL,
29 | AttributedTo: developerURL,
30 | Audience: vocab.ItemCollection{vocab.PublicNS},
31 | Content: nil, //vocab.NaturalLanguageValues{{Ref: vocab.NilLangRef, Value: ""}},
32 | Summary: vocab.NaturalLanguageValuesNew(vocab.DefaultLangRef("Generic ActivityPub service")),
33 | Tag: nil,
34 | URL: baseURL,
35 | Endpoints: &vocab.Endpoints{
36 | OauthAuthorizationEndpoint: vocab.IRI(fmt.Sprintf("%s/authorize", oauth.String())),
37 | OauthTokenEndpoint: vocab.IRI(fmt.Sprintf("%s/token", oauth.String())),
38 | },
39 | }
40 |
41 | s.Inbox = vocab.Inbox.IRI(s)
42 | s.Outbox = vocab.Outbox.IRI(s)
43 | s.Streams = vocab.ItemCollection{
44 | filters.ActorsType.IRI(s),
45 | filters.ActivitiesType.IRI(s),
46 | filters.ObjectsType.IRI(s),
47 | }
48 | return s
49 | }
50 |
51 | func DefaultServiceIRI(baseURL string) vocab.IRI {
52 | u, _ := url.Parse(baseURL)
53 | // TODO(marius): I don't like adding the / folder to something like http://fedbox.git
54 | if u.Path == "" {
55 | u.Path = "/"
56 | }
57 | return vocab.IRI(u.String())
58 | }
59 |
60 | func LoadActor(st processing.ReadStore, iri vocab.IRI, ff ...filters.Check) (vocab.Actor, error) {
61 | var act vocab.Actor
62 |
63 | selfCol, err := st.Load(iri, ff...)
64 | if err != nil {
65 | return act, errors.Annotatef(err, "invalid service IRI %s", iri)
66 | }
67 |
68 | err = vocab.OnActor(selfCol, func(actor *vocab.Actor) error {
69 | act = *actor
70 | return nil
71 | })
72 | return act, err
73 | }
74 |
75 | // GenerateID generates a unique identifier for the 'it' [vocab.Item].
76 | func GenerateID(it vocab.Item, partOf vocab.IRI, by vocab.Item) (vocab.ID, error) {
77 | uid := uuid.New()
78 | id := partOf.GetLink().AddPath(uid)
79 | typ := it.GetType()
80 | if vocab.ActivityTypes.Contains(typ) || vocab.IntransitiveActivityTypes.Contains(typ) {
81 | err := vocab.OnIntransitiveActivity(it, func(a *vocab.IntransitiveActivity) error {
82 | if rec := a.Recipients(); rec.Contains(vocab.PublicNS) {
83 | return nil
84 | }
85 | if vocab.IsNil(by) {
86 | by = a.Actor
87 | }
88 | if !vocab.IsNil(by) {
89 | // if "it" is not a public activity, save it to its actor Outbox instead of the global activities collection
90 | outbox := vocab.Outbox.IRI(by)
91 | id = vocab.ID(fmt.Sprintf("%s/%s", outbox, uid))
92 | }
93 | return nil
94 | })
95 | if err != nil {
96 | return id, err
97 | }
98 | err = vocab.OnObject(it, func(a *vocab.Object) error {
99 | a.ID = id
100 | return nil
101 | })
102 | return id, err
103 | }
104 | if it.IsLink() {
105 | return id, vocab.OnLink(it, func(l *vocab.Link) error {
106 | l.ID = id
107 | return nil
108 | })
109 | }
110 | return id, vocab.OnObject(it, func(o *vocab.Object) error {
111 | o.ID = id
112 | return nil
113 | })
114 | return id, nil
115 | }
116 |
--------------------------------------------------------------------------------
/activitypub/activitypub_test.go:
--------------------------------------------------------------------------------
1 | package activitypub
2 |
3 | import (
4 | "reflect"
5 | "strings"
6 | "testing"
7 |
8 | vocab "github.com/go-ap/activitypub"
9 | )
10 |
11 | func TestItemByType(t *testing.T) {
12 | type testPairs map[vocab.ActivityVocabularyType]reflect.Type
13 |
14 | var collectionPtrType = reflect.TypeOf(new(*vocab.Collection)).Elem()
15 | var orderedCollectionPtrType = reflect.TypeOf(new(*vocab.OrderedCollection)).Elem()
16 |
17 | var tests = testPairs{
18 | vocab.CollectionType: collectionPtrType,
19 | vocab.OrderedCollectionType: orderedCollectionPtrType,
20 | }
21 |
22 | for typ, test := range tests {
23 | t.Run(string(typ), func(t *testing.T) {
24 | v, err := vocab.GetItemByType(typ)
25 | if err != nil {
26 | t.Error(err)
27 | }
28 | if reflect.TypeOf(v) != test {
29 | t.Errorf("Invalid type returned %T, expected %s", v, test.String())
30 | }
31 | })
32 | }
33 | }
34 |
35 | func TestGenerateID(t *testing.T) {
36 | var generateIDTests vocab.ActivityVocabularyTypes
37 | generateIDTests = append(generateIDTests, vocab.ObjectTypes...)
38 | generateIDTests = append(generateIDTests, vocab.ActivityTypes...)
39 | generateIDTests = append(generateIDTests, vocab.ActorTypes...)
40 | partOf := vocab.IRI("http://example.com")
41 | for _, typ := range generateIDTests {
42 | it, err := vocab.GetItemByType(typ)
43 | if err != nil {
44 | t.Errorf("Unable to create object from type: %s", err)
45 | }
46 | id, err := GenerateID(it, partOf, nil)
47 | if err != nil {
48 | t.Errorf("GenerateID failed: %s", err)
49 | }
50 | if !strings.Contains(string(id), partOf.String()) {
51 | t.Errorf("Invalid ID: %s, does not contain base URL %s", id, partOf)
52 | }
53 | if id != it.GetID() {
54 | t.Errorf("IDs don't match: %s, expected %s", it.GetID(), id)
55 | }
56 | }
57 | }
58 |
59 | func TestDefaultServiceIRI(t *testing.T) {
60 | t.Skipf("TODO")
61 | }
62 |
63 | func TestSelf(t *testing.T) {
64 | testURL := "http://example.com:666"
65 | s := Self(vocab.IRI(testURL))
66 |
67 | if s.ID != vocab.ID(testURL) {
68 | t.Errorf("Invalid ID %s, expected %s", s.ID, testURL)
69 | }
70 | if s.Type != vocab.ServiceType {
71 | t.Errorf("Invalid Type %s, expected %s", s.Type, vocab.ServiceType)
72 | }
73 | if !s.Name.First().Value.Equals(vocab.Content("self")) {
74 | t.Errorf("Invalid Name %s, expected %s", s.Name, "self")
75 | }
76 | if s.AttributedTo.GetLink() != "https://github.com/mariusor" {
77 | t.Errorf("Invalid AttributedTo %s, expected %s", s.AttributedTo, "https://github.com/mariusor")
78 | }
79 | if s.Audience.First().GetLink() != vocab.PublicNS {
80 | t.Errorf("Invalid Audience %s, expected %s", s.Audience.First(), vocab.PublicNS)
81 | }
82 | if s.Content != nil {
83 | t.Errorf("Invalid Audience %s, expected %v", s.Content, nil)
84 | }
85 | if s.Icon != nil {
86 | t.Errorf("Invalid Icon %s, expected %v", s.Icon, nil)
87 | }
88 | if s.Image != nil {
89 | t.Errorf("Invalid Image %s, expected %v", s.Image, nil)
90 | }
91 | if s.Location != nil {
92 | t.Errorf("Invalid Location %s, expected %v", s.Location, nil)
93 | }
94 | if !s.Summary.First().Value.Equals(vocab.Content("Generic ActivityPub service")) {
95 | t.Errorf("Invalid Summary %s, expected %v", s.Summary, "Generic ActivityPub service")
96 | }
97 | if s.Tag != nil {
98 | t.Errorf("Invalid Tag %s, expected %v", s.Tag, nil)
99 | }
100 | testIRI := vocab.IRI(testURL)
101 | if s.URL != testIRI {
102 | t.Errorf("Invalid URL %s, expected %v", s.URL, testURL)
103 | }
104 | inb := vocab.Inbox.IRI(testIRI)
105 | if s.Inbox != inb {
106 | t.Errorf("Invalid Inbox %s, expected %v", s.Inbox, inb)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/app_test.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import (
4 | "testing"
5 |
6 | "git.sr.ht/~mariusor/lw"
7 | "github.com/go-ap/fedbox/internal/config"
8 | fs "github.com/go-ap/storage-fs"
9 | )
10 |
11 | var defaultConfig = config.Options{
12 | Storage: config.StorageFS,
13 | }
14 |
15 | func TestNew(t *testing.T) {
16 | store, err := fs.New(fs.Config{Path: t.TempDir()})
17 | if err != nil {
18 | t.Errorf("unable to initialize fs storage: %s", err)
19 | }
20 | app, err := New(lw.Dev(), config.Options{BaseURL: "http://example.com"}, store)
21 | if err != nil {
22 | t.Errorf("Environment 'test' should not trigger an error: %s", err)
23 | }
24 | if app == nil {
25 | t.Errorf("Nil app pointer returned by New")
26 | }
27 | }
28 |
29 | func TestFedbox_Config(t *testing.T) {
30 | t.Skipf("TODO")
31 | }
32 |
33 | func TestFedbox_Run(t *testing.T) {
34 | t.Skipf("TODO")
35 | }
36 |
37 | func TestFedbox_Stop(t *testing.T) {
38 | t.Skipf("TODO")
39 | }
40 |
--------------------------------------------------------------------------------
/cmd/control/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime/debug"
7 |
8 | "github.com/go-ap/fedbox/internal/cmd"
9 | "github.com/go-ap/fedbox/internal/config"
10 | "github.com/go-ap/fedbox/internal/env"
11 | "github.com/urfave/cli/v2"
12 | )
13 |
14 | var Version = "HEAD"
15 |
16 | func main() {
17 | app := cli.App{}
18 | app.Name = "fedboxctl"
19 | app.Usage = "helper utility to manage a FedBOX instance"
20 | if build, ok := debug.ReadBuildInfo(); ok && Version == "HEAD" {
21 | app.Version = build.Main.Version
22 | }
23 | app.Before = cmd.Before
24 | app.Flags = []cli.Flag{
25 | &cli.StringFlag{
26 | Name: "url",
27 | Usage: "The url used by the application",
28 | },
29 | &cli.StringFlag{
30 | Name: "env",
31 | Usage: fmt.Sprintf("The environment to use. Possible values: %q", []env.Type{env.DEV, env.QA, env.PROD}),
32 | Value: string(env.DEV),
33 | },
34 | &cli.BoolFlag{
35 | Name: "verbose",
36 | Usage: fmt.Sprintf("Increase verbosity level from the default associated with the environment settings."),
37 | },
38 | &cli.StringFlag{
39 | Name: "type",
40 | Usage: fmt.Sprintf("Type of the backend to use. Possible values: %q", []config.StorageType{config.StorageBoltDB, config.StorageBadger, config.StorageFS}),
41 | },
42 | &cli.StringFlag{
43 | Name: "path",
44 | Value: ".",
45 | Usage: fmt.Sprintf("The path for the storage folder or socket"),
46 | },
47 | &cli.StringFlag{
48 | Name: "user",
49 | Value: "fedbox",
50 | Usage: "The postgres database user",
51 | },
52 | }
53 | app.Commands = []*cli.Command{
54 | cmd.PubCmd,
55 | cmd.OAuth2Cmd,
56 | cmd.BootstrapCmd,
57 | cmd.AccountsCmd,
58 | cmd.FixStorageCollectionsCmd,
59 | cmd.Reload, cmd.Maintenance, cmd.Stop,
60 | }
61 |
62 | if err := app.Run(os.Args); err != nil {
63 | _, _ = fmt.Fprintln(os.Stderr, err)
64 | os.Exit(1)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/cmd/fedbox/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "runtime/debug"
6 |
7 | "github.com/go-ap/fedbox/internal/cmd"
8 | )
9 |
10 | var version = "HEAD"
11 |
12 | func main() {
13 | if build, ok := debug.ReadBuildInfo(); ok && version == "HEAD" && build.Main.Version != "(devel)" {
14 | version = build.Main.Version
15 | }
16 | if err := cmd.NewApp(version).Run(os.Args); err != nil {
17 | cmd.Errf(err.Error())
18 | os.Exit(1)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/doc/INSTALL.md:
--------------------------------------------------------------------------------
1 | ## Getting the source
2 |
3 | ```sh
4 | $ git clone https://github.com/go-ap/fedbox
5 | $ cd fedbox
6 | ```
7 |
8 | ## Compiling
9 |
10 | ```sh
11 | $ make all
12 | ```
13 |
14 | Compiling for a specific storage backend:
15 |
16 | ```shell
17 | $ make STORAGE=sqlite all
18 | ```
19 |
20 | Compiling for the production environment:
21 |
22 | ```shell
23 | $ make ENV=prod all
24 | ```
25 |
26 | ## Editing the configuration
27 |
28 | ```sh
29 | $ cp .env.dist .env
30 | $ $EDITOR .env
31 | ```
32 |
33 | ## Bootstrapping
34 |
35 | This step ensures that the storage method we're using gets initialized.
36 |
37 | ```sh
38 | $ ./bin/fedboxctl bootstrap
39 | ```
40 |
41 | For a more advanced example, the [`tools/bootstrap.sh`](../tools/bootstrap.sh) script has a more elaborate use case to
42 | automate bootstrapping a project together with adding an Actor and an OAuth2 client.
43 |
44 | ## Containers
45 |
46 | See the [containers](../images/README.md) document for details about how to build podman/docker images or use the ready made ones.
47 |
--------------------------------------------------------------------------------
/doc/c2s.md:
--------------------------------------------------------------------------------
1 | # FedBOX as an ActivityPub server supporting C2S interactions
2 |
3 | Here I will do a dump of my assumptions regarding how the "client to server[1]" (or C2S) interactions should work on FedBOX.
4 |
5 | The first one is that the C2S API will be structured as a REST(ful) API in respect to addressing objects.
6 |
7 | This means that every object on the server will have an unique URL that can be addressed at.
8 |
9 | This type of URL is called an "Internationalized Resource Identifier[2]" (or IRI) in the ActivityPub spec.
10 |
11 | It also represents the Object's ID in respect to the AP spec.
12 |
13 | ## API end-points
14 |
15 | FedBOX has as a unique entry point for any non-authorized request. For convenience we'll assume that is the root path for the domain (eg: `https://federated.id/`)
16 |
17 | We'll call this entry point the "Local Service's IRI", as it response consists of a Service Actor representing the current instance.
18 |
19 | The Service, as an Actor, must have an Inbox collection, which we expose in the `https://federated.id/inbox` end-point.
20 |
21 | It also represents the shared inbox for all actors created on the service.
22 |
23 | ## Collections
24 |
25 | Since in the ActivityPub spec there is no schema for IRI generation for Objects, Activities and Actors and it's left as an implementation detail, we decided that for FedBOX we wanted to create three non-specified collection end-points, corresponding to each of these and serving as a base for every every entity's ID.
26 |
27 | These additional non-spec conforming collections are:
28 |
29 | * https://federated.id/actors - where we can query all the actors on the instance.
30 | * https://federated.id/activities - where we can query all the activities on the instance.
31 | * https://federated.id/objects - where we can query the rest of the object types.
32 |
33 | ## Object collections:
34 |
35 | An object collection, represents any collection that contains only ActivityPub Objects.
36 | The object collections in the ActivitypPub spec are: `following`, `followers`, `liked`.
37 | Additionally FedBOX has the previously mentioned `/actors` and `/objects` root end-points.
38 |
39 | On these collections we can use the following filters:
40 |
41 | * **iri**: list of IRIs representing specific object ID's we want to load
42 | * **type**: list of Object types
43 | * **to**: list of IRIs
44 | * **cc**: list of IRIs
45 | * **audience**: list of IRIs
46 | * **url**: list of URLs
47 |
48 | ## Activity collections:
49 |
50 | An activity collection, represents any collection that contains only ActivityPub Activities.
51 | The activity collections in the ActivitypPub spec are: `outbox`, `inbox`, `likes`, `shares`, `replies`.
52 | Additionally FedBOX supports the `/activities` root end-point.
53 |
54 | In order to get the full representation of the items, after loading one of these collections, their Object properties need to be dereferenced and loaded again.
55 |
56 | Besides the filters applicable to Object collections we have also:
57 |
58 | * **actor**: list of IRIs
59 | * **object** list of IRIs
60 | * **target**: list of IRIs
61 |
62 | # The filtering
63 |
64 | Filtering collections is done using query parameters corresponding to the snakeCased value of the property's name it matches against.
65 |
66 | For end-points that return collection of activities, filtering can be done on the activity's actor/object/target properties
67 | by composing the filter name with a matching prefix for the child property:
68 |
69 | Eg:
70 | `object.url=https://example.fed`
71 | `object.iri=https://example.fed/objects/{uuid}`
72 |
73 | All filters can be used multiple times in the URL.
74 |
75 | The matching logic is:
76 |
77 | * Multiple values of same filter are matched by doing a union on the resulting sets.
78 | * Different filters keys match by doing an intersection on the resulting sets of each filter.
79 |
80 | The filtering values support basic operators to be used. They are:
81 |
82 | For string based properties:
83 |
84 | * `key=~value` - match `value` as a substring
85 | * `key=!value` - match everything that's doesn't contain `value` as a substring
86 |
87 | For date based properties
88 |
89 | * `key=>value` - match everything that has `key` property after the `value`
90 | * `key= github.com/census-instrumentation/opencensus-go v0.23.0
80 |
--------------------------------------------------------------------------------
/handlers_test.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import "testing"
4 |
5 | func TestHandleCollection(t *testing.T) {
6 | t.Skipf("TODO")
7 | }
8 |
9 | func TestHandleItem(t *testing.T) {
10 | t.Skipf("TODO")
11 | }
12 |
13 | func TestHandleRequest(t *testing.T) {
14 | t.Skipf("TODO")
15 | }
16 |
--------------------------------------------------------------------------------
/httpsig.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "crypto/ecdsa"
7 | "crypto/ed25519"
8 | "crypto/rsa"
9 | "io"
10 | "net/http"
11 | "time"
12 |
13 | "git.sr.ht/~mariusor/lw"
14 | vocab "github.com/go-ap/activitypub"
15 | "github.com/go-ap/errors"
16 | "github.com/go-ap/processing"
17 | "github.com/go-fed/httpsig"
18 | )
19 |
20 | var (
21 | digestAlgorithm = httpsig.DigestSha256
22 | headersToSign = []string{httpsig.RequestTarget, "Host", "Date"}
23 | signatureExpiration = int64(time.Hour.Seconds())
24 | )
25 |
26 | type signer struct {
27 | signers map[httpsig.Algorithm]httpsig.Signer
28 | logger lw.Logger
29 | }
30 |
31 | func (s signer) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error {
32 | algs := make([]string, 0)
33 | for a, v := range s.signers {
34 | algs = append(algs, string(a))
35 | if err := v.SignRequest(pKey, pubKeyId, r, body); err == nil {
36 | return nil
37 | } else {
38 | s.logger.Warnf("invalid signer algo %s:%T %+s", a, v, err)
39 | }
40 | }
41 | return errors.Newf("no suitable request signer for public key[%T] %s, tried %+v", pKey, pubKeyId, algs)
42 | }
43 |
44 | func newSigner(pubKey crypto.PrivateKey, headers []string, l lw.Logger) (signer, error) {
45 | s := signer{logger: l}
46 | s.signers = make(map[httpsig.Algorithm]httpsig.Signer, 0)
47 |
48 | algos := make([]httpsig.Algorithm, 0)
49 | switch pubKey.(type) {
50 | case *rsa.PrivateKey:
51 | algos = append(algos, httpsig.RSA_SHA256, httpsig.RSA_SHA512)
52 | case *ecdsa.PrivateKey:
53 | algos = append(algos, httpsig.ECDSA_SHA512, httpsig.ECDSA_SHA256)
54 | case ed25519.PrivateKey:
55 | algos = append(algos, httpsig.ED25519)
56 | }
57 | for _, alg := range algos {
58 | sig, alg, err := httpsig.NewSigner([]httpsig.Algorithm{alg}, digestAlgorithm, headers, httpsig.Signature, signatureExpiration)
59 | if err == nil {
60 | s.signers[alg] = sig
61 | }
62 | }
63 | return s, nil
64 | }
65 |
66 | func s2sSignFn(actorID vocab.IRI, keyLoader processing.KeyLoader, l lw.Logger) func(r *http.Request) error {
67 | key, err := keyLoader.LoadKey(actorID)
68 | if err != nil {
69 | return func(r *http.Request) error {
70 | return err
71 | }
72 | }
73 | return func(r *http.Request) error {
74 | headers := headersToSign
75 | if r.Method == http.MethodPost {
76 | headers = append(headers, "Digest")
77 | }
78 |
79 | s, err := newSigner(key, headers, l)
80 | if err != nil {
81 | return errors.Annotatef(err, "unable to initialize HTTP signer")
82 | }
83 | // NOTE(marius): this is needed to accommodate for the FedBOX service user which usually resides
84 | // at the root of a domain, and it might miss a valid path. This trips the parsing of keys with id
85 | // of form https://example.com#main-key
86 | u, _ := actorID.URL()
87 | if u.Path == "" {
88 | u.Path = "/"
89 | }
90 | u.Fragment = "main-key"
91 | keyId := u.String()
92 | bodyBuf := bytes.Buffer{}
93 | if r.Body != nil {
94 | if _, err := io.Copy(&bodyBuf, r.Body); err == nil {
95 | r.Body = io.NopCloser(&bodyBuf)
96 | }
97 | }
98 | return s.SignRequest(key, keyId, r, bodyBuf.Bytes())
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/images/.env.default:
--------------------------------------------------------------------------------
1 | STORAGE=fs
2 | STORAGE_PATH=/storage
3 | HTTPS=true
4 | HOSTNAME=fedbox
5 | KEY_PATH=/etc/ssl/certs/fedbox.key
6 | CERT_PATH=/etc/ssl/certs/fedbox.crt
7 |
--------------------------------------------------------------------------------
/images/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .env.dev
3 | *.crt
4 | *.key
5 | *.pem
6 |
--------------------------------------------------------------------------------
/images/Makefile:
--------------------------------------------------------------------------------
1 | SHELL := bash
2 | .ONESHELL:
3 | .SHELLFLAGS := -eu -o pipefail -c
4 | .DELETE_ON_ERROR:
5 | MAKEFLAGS += --warn-undefined-variables
6 | MAKEFLAGS += --no-builtin-rules
7 |
8 | ENV ?= dev
9 | FEDBOX_HOSTNAME ?= fedbox
10 | PORT ?= 4000
11 | STORAGE ?= all
12 | STORAGE_PATH ?= $(shell realpath .cache)
13 | STORAGE_OBJECTS = $(STORAGE_PATH)/objects
14 | TAG ?= $(ENV)
15 | VERSION ?= HEAD
16 |
17 |
18 | TAG_CMD=podman tag
19 | PUSH_CMD=podman push
20 |
21 | ifeq ($(shell git describe --always > /dev/null 2>&1 ; echo $$?), 0)
22 | BRANCH=$(shell git rev-parse --abbrev-ref HEAD | tr '/' '-')
23 | HASH=$(shell git rev-parse --short HEAD)
24 | VERSION = $(shell printf "%s-%s" "$(BRANCH)" "$(HASH)")
25 | endif
26 | ifeq ($(shell git describe --tags > /dev/null 2>&1 ; echo $$?), 0)
27 | VERSION = $(shell git describe --tags | tr '/' '-')
28 | endif
29 |
30 | ifneq ($(STORAGE),all)
31 | TAG=$(ENV)-$(STORAGE)
32 | endif
33 |
34 | .PHONY: clean build builder push cert
35 |
36 | $(FEDBOX_HOSTNAME).pem:
37 | ./gen-certs.sh $(FEDBOX_HOSTNAME)
38 |
39 | cert: $(FEDBOX_HOSTNAME).pem
40 |
41 | clean:
42 | @-$(RM) $(FEDBOX_HOSTNAME).{key,crt,pem}
43 |
44 | builder:
45 | ./build.sh .. fedbox/builder
46 |
47 | build:
48 | ENV=$(ENV) VERSION=$(VERSION) STORAGE=$(STORAGE) PORT=$(PORT) HOSTNAME=$(FEDBOX_HOSTNAME) ./image.sh $(FEDBOX_HOSTNAME)/app:$(TAG)
49 |
50 | push: build
51 | $(TAG_CMD) $(FEDBOX_HOSTNAME)/app:$(TAG) quay.io/go-ap/fedbox:$(TAG)
52 | $(PUSH_CMD) quay.io/go-ap/fedbox:$(TAG)
53 | ifeq ($(TAG),dev)
54 | $(TAG_CMD) $(FEDBOX_HOSTNAME)/app:$(TAG) quay.io/go-ap/fedbox:latest || true
55 | $(PUSH_CMD) quay.io/go-ap/fedbox:latest || true
56 | endif
57 | ifneq ($(VERSION),HEAD)
58 | $(TAG_CMD) $(FEDBOX_HOSTNAME)/app:$(TAG) quay.io/go-ap/fedbox:$(VERSION)-$(TAG) || true
59 | $(PUSH_CMD) quay.io/go-ap/fedbox:$(VERSION)-$(TAG) || true
60 | endif
61 |
--------------------------------------------------------------------------------
/images/README.md:
--------------------------------------------------------------------------------
1 | ## Container images
2 |
3 | We are building podman[^1] container images for FedBOX that can be found at [quay.io](https://quay.io/go-ap/fedbox).
4 |
5 | The containers are split onto two dimensions:
6 | * run environment type: `prod`, `qa` or `dev`:
7 | * `dev` images are built with all debugging information and logging context possible, built for every push.
8 | * `qa` images are built by stripping the binaries of unneeded elements. Less debugging and logging possible,
9 | also built every push.
10 | * `prod` images are similar to `qa` ones but are created only when a tagged version of the project is released.
11 | * storage type: `fs`, `sqlite`, `boltdb`, or all.
12 | * `fs` the JSON-Ld documents are saved verbatim on disk in a tree folder structure. Fast and error prone.
13 | * `sqlite`: the JSON-Ld documents are saved in a key-value store under the guise of a database table.
14 | Querying large collections could be slow.
15 | * `boltdb`: a more traditional key-value store in the Go ecosystem.
16 |
17 | A resulting image tag has information about all of these, and it would look like `qa-boltdb`
18 | (stripped image supporting boltdb storage), or `dev` (not stripped image with all storage options available).
19 |
20 | The `tools/run-container` script can be used as an example of how to run such a container.
21 |
22 | ```sh
23 | # /var/cache/fedbox must exist and be writable as current user
24 | # /var/cache/fedbox/env must be a valid env file as shown in the INSTALL document.
25 | $ podman run --network=host --name=FedBOX -v /var/cache/fedbox/env:/.env -v /var/cache/fedbox:/storage --env-file=/var/cache/fedbox/env quay.io/go-ap/fedbox:latest
26 | ```
27 |
28 | ### Running *ctl commands in the containers
29 |
30 | ```sh
31 | # running with the same configuration environment as above
32 | $ podman exec --env-file=/var/cache/fedbox/env FedBOX fedboxctl bootstrap
33 | $ podman exec --env-file=/var/cache/fedbox/env FedBOX fedboxctl pub actor add --type Application
34 | Enter the actor's name: test
35 | test's pw:
36 | pw again:
37 | Added "Application" [test]: https://fedbox/actors/22200000-0000-0000-0001-93e066611fcb
38 | ```
39 |
40 | [^1] And docker, of course.
41 |
42 |
--------------------------------------------------------------------------------
/images/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | _workdir=${1:-../}
6 | _image_name=${2:-fedbox/builder}
7 |
8 | _context=$(realpath "${_workdir}")
9 |
10 | _builder=$(buildah from docker.io/library/golang:1.24-alpine)
11 |
12 | buildah run "${_builder}" /sbin/apk update
13 | buildah run "${_builder}" /sbin/apk add make bash openssl upx
14 |
15 | buildah config --env GO111MODULE=on "${_builder}"
16 | buildah config --env GOWORK=off "${_builder}"
17 |
18 | buildah copy --ignorefile "${_context}/.containerignore" --contextdir "${_context}" "${_builder}" "${_context}" /go/src/app
19 |
20 | buildah config --workingdir /go/src/app "${_builder}"
21 |
22 | buildah run "${_builder}" make go.sum
23 | buildah run "${_builder}" go mod vendor
24 |
25 | buildah commit "${_builder}" "${_image_name}"
26 |
--------------------------------------------------------------------------------
/images/gen-certs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | APP_HOSTNAME="${1}"
3 |
4 | openssl req \
5 | -subj "/C=AQ/ST=Omond/L=Omond/O=${APP_HOSTNAME}/OU=none/CN=${APP_HOSTNAME}" \
6 | -newkey rsa:2048 -sha256 \
7 | -keyout "${APP_HOSTNAME}.key" \
8 | -nodes -x509 -days 365 \
9 | -out "${APP_HOSTNAME}.crt" && \
10 | cat "${APP_HOSTNAME}.key" "${APP_HOSTNAME}.crt" > "${APP_HOSTNAME}.pem"
11 |
--------------------------------------------------------------------------------
/images/image.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | _environment=${ENV:-dev}
6 | _hostname=${HOSTNAME:-fedbox}
7 | _listen_port=${PORT:-4000}
8 | _storage=${STORAGE:-all}
9 | _version=${VERSION:-HEAD}
10 |
11 | _image_name=${1:-"${_hostname}:${_environment}-${_storage}"}
12 | _build_name=${2:-localhost/fedbox/builder}
13 |
14 | _builder=$(buildah from "${_build_name}":latest)
15 | if [[ -z "${_builder}" ]]; then
16 | echo "Unable to find builder image: ${_build_name}"
17 | exit 1
18 | fi
19 |
20 | echo "Building image ${_image_name} for host=${_hostname} env:${_environment} storage:${_storage} version:${_version} port:${_listen_port}"
21 |
22 | buildah run "${_builder}" make ENV="${_environment}" STORAGE="${_storage}" VERSION="${_version}" all
23 | buildah run "${_builder}" make -C images "${_hostname}.pem"
24 |
25 | _image=$(buildah from gcr.io/distroless/static:latest)
26 |
27 | buildah config --env ENV="${_environment}" "${_image}"
28 | buildah config --env HOSTNAME="${_hostname}" "${_image}"
29 | buildah config --env LISTEN=:"${_listen_port}" "${_image}"
30 | buildah config --env KEY_PATH=/etc/ssl/certs/"${_hostname}.key" "${_image}"
31 | buildah config --env CERT_PATH=/etc/ssl/certs/"${_hostname}.crt" "${_image}"
32 | buildah config --env HTTPS=true "${_image}"
33 | buildah config --env STORAGE="${_storage}" "${_image}"
34 |
35 | buildah config --port "${_listen_port}" "${_image}"
36 |
37 | buildah config --volume /storage "${_image}"
38 | buildah config --volume /.env "${_image}"
39 |
40 | buildah copy --from "${_builder}" "${_image}" /go/src/app/bin/* /bin/
41 | buildah copy --from "${_builder}" "${_image}" "/go/src/app/images/${_hostname}.key" /etc/ssl/certs/
42 | buildah copy --from "${_builder}" "${_image}" "/go/src/app/images/${_hostname}.crt" /etc/ssl/certs/
43 | buildah copy --from "${_builder}" "${_image}" "/go/src/app/images/${_hostname}.pem" /etc/ssl/certs/
44 |
45 | buildah config --entrypoint '["/bin/fedbox"]' "${_image}"
46 |
47 | # commit
48 | buildah commit "${_image}" "${_image_name}"
49 |
--------------------------------------------------------------------------------
/integration/build.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | "strconv"
9 |
10 | "github.com/containers/buildah"
11 | "github.com/containers/buildah/define"
12 | "github.com/containers/common/pkg/config"
13 | stt "github.com/containers/image/v5/storage"
14 | "github.com/containers/image/v5/transports/alltransports"
15 | "github.com/containers/storage"
16 | "github.com/containers/storage/pkg/idtools"
17 | "github.com/containers/storage/pkg/unshare"
18 | "github.com/containers/storage/types"
19 | "github.com/sirupsen/logrus"
20 | )
21 |
22 | const (
23 | //baseImage = "cgr.dev/chainguard/static:latest"
24 | baseImage = "gcr.io/distroless/static:latest"
25 | targetRepo = "localhost"
26 | containerName = "fedbox"
27 | importPath = "github.com/go-ap/fedbox"
28 | commitSHA = "deadbeef"
29 | )
30 |
31 | var basePath = filepath.Join(os.TempDir(), "fedbox-test")
32 |
33 | func buildImage(ctx context.Context, verbose bool) (string, error) {
34 | logrus.SetOutput(os.Stderr)
35 | if verbose {
36 | logrus.SetLevel(logrus.DebugLevel)
37 | }
38 | logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true, DisableQuote: true, ForceColors: true})
39 |
40 | //logger := logrus.New()
41 |
42 | buildah.InitReexec()
43 |
44 | buildStoreOptions, err := storage.DefaultStoreOptions()
45 | if err != nil {
46 | return "", err
47 | }
48 |
49 | buildStoreOptions.RunRoot = filepath.Join(basePath, "root")
50 | buildStoreOptions.GraphRoot = filepath.Join(basePath, "graph")
51 | buildStoreOptions.RootlessStoragePath = filepath.Join(basePath, "rootless")
52 | //buildStoreOptions.GraphDriverName = "vfs"
53 | //buildStoreOptions.GraphDriverName = "aufs"
54 | buildStoreOptions.GraphDriverName = "overlay"
55 | buildStoreOptions.GraphDriverOptions = []string{
56 | "skip_mount_home=true",
57 | //"ignore_chown_errors=true",
58 | //"use_composefs=true", // error building: composefs is not supported in user namespaces
59 | }
60 | //buildStoreOptions.DisableVolatile = true
61 | //buildStoreOptions.TransientStore = true
62 | buildStoreOptions.UIDMap = stt.Transport.DefaultUIDMap()
63 | buildStoreOptions.GIDMap = stt.Transport.DefaultGIDMap()
64 |
65 | store, err := storage.GetStore(buildStoreOptions)
66 | if err != nil {
67 | return "", err
68 | }
69 |
70 | _ = os.Setenv(unshare.UsernsEnvName, "done")
71 | //netOpts := netavark.InitConfig{
72 | // Config: &config.Config{},
73 | // NetworkConfigDir: filepath.Join(basePath, "net", "config"),
74 | // NetworkRunDir: filepath.Join(basePath, "net", "run"),
75 | // NetavarkBinary: "true",
76 | //}
77 | //net, err := netavark.NewNetworkInterface(&netOpts)
78 | //if err != nil {
79 | // return "", err
80 | //}
81 | //
82 | commonBuildOpts := buildah.CommonBuildOptions{}
83 | //defaultEnv := []string{}
84 |
85 | // NOTE(marius): this fails with a mounting error.
86 | // The internet seems to suggest we need to force a user namespace creation when running rootless,
87 | // but I don't know how to do this programmatically, and they don't give any clues:
88 | // https://github.com/containers/buildah/issues/5744
89 | // https://github.com/containers/buildah/issues/3948
90 | //namespaces, err := buildah.DefaultNamespaceOptions()
91 | //if err != nil {
92 | // return "", err
93 | //}
94 | //namespaces.AddOrReplace(define.NamespaceOption{Name: string(specs.MountNamespace), Host: true})
95 |
96 | conf, err := config.Default()
97 | if err != nil {
98 | return "", err
99 | }
100 |
101 | uidStr := strconv.Itoa(os.Geteuid())
102 | capabilities, err := conf.Capabilities(uidStr, nil, nil)
103 | if err != nil {
104 | return "", err
105 | }
106 |
107 | buildOpts := buildah.BuilderOptions{
108 | Args: nil,
109 | FromImage: baseImage,
110 | Capabilities: capabilities,
111 | Container: containerName,
112 | //Logger: logger,
113 | //Mount: true,
114 | ReportWriter: os.Stderr,
115 | Isolation: buildah.IsolationOCIRootless,
116 | //NamespaceOptions: namespaces,
117 | //ConfigureNetwork: 0,
118 | //NetworkInterface: net,
119 | IDMappingOptions: &define.IDMappingOptions{
120 | AutoUserNs: true,
121 | AutoUserNsOpts: types.AutoUserNsOptions{
122 | Size: 4096,
123 | InitialSize: 1024,
124 | AdditionalUIDMappings: []idtools.IDMap{
125 | {ContainerID: 10000, HostID: 1, Size: 4096},
126 | },
127 | AdditionalGIDMappings: []idtools.IDMap{
128 | {ContainerID: 10000, HostID: 1, Size: 4096},
129 | },
130 | },
131 | },
132 | CommonBuildOpts: &commonBuildOpts,
133 | //Format: "",
134 | //Devices: nil,
135 | //DeviceSpecs: nil,
136 | //DefaultEnv: defaultEnv,
137 | }
138 | builder, err := buildah.NewBuilder(ctx, store, buildOpts)
139 | if err != nil {
140 | return "", err
141 | }
142 |
143 | img, err := alltransports.ParseImageName("localhost/fedbox/app:test")
144 | if err != nil {
145 | return "", err
146 | }
147 | commitOpts := buildah.CommitOptions{
148 | //PreferredManifestType: "",
149 | //Compression: archive.Gzip,
150 | //AdditionalTags: nil,
151 | ReportWriter: os.Stderr,
152 | //HistoryTimestamp: nil,
153 | //SystemContext: nil,
154 | //IIDFile: "",
155 | //Squash: false,
156 | //SignBy: "",
157 | //Manifest: "",
158 | //ExtraImageContent: nil,
159 | }
160 | hash, canonical, digest, err := builder.Commit(ctx, img, commitOpts)
161 | if err != nil {
162 | return "", err
163 | }
164 | fmt.Printf("hash: %s\n", hash)
165 | fmt.Printf("digest: %s\n", digest)
166 | fmt.Printf("canonical: %s\n", canonical)
167 | return hash, nil
168 | }
169 |
--------------------------------------------------------------------------------
/integration/c2s_test.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "net/http"
7 | "testing"
8 |
9 | vocab "github.com/go-ap/activitypub"
10 | )
11 |
12 | var httpClient = http.Client{
13 | Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
14 | }
15 |
16 | func Test_Fetch(t *testing.T) {
17 | type wanted struct {
18 | status int
19 | item vocab.Item
20 | }
21 | tests := []struct {
22 | name string
23 | arg vocab.IRI
24 | wanted wanted
25 | }{
26 | {
27 | name: "FedBOX root",
28 | arg: "https://fedbox",
29 | wanted: wanted{status: http.StatusOK},
30 | },
31 | {
32 | name: "FedBOX Admin",
33 | arg: "https://fedbox/actors/1",
34 | wanted: wanted{status: http.StatusOK},
35 | },
36 | {
37 | name: "sysop tag",
38 | arg: "https://fedbox/objects/0",
39 | wanted: wanted{status: http.StatusOK},
40 | },
41 | {
42 | name: "object 1",
43 | arg: "https://fedbox/objects/1",
44 | wanted: wanted{status: http.StatusOK},
45 | },
46 | }
47 |
48 | ctx := context.Background()
49 | mocks, err := initMocks(ctx, t, suite{name: "fedbox"})
50 | if err != nil {
51 | t.Fatalf("unable to initialize containers: %s", err)
52 | }
53 |
54 | t.Cleanup(func() {
55 | mocks.cleanup(t)
56 | })
57 |
58 | for _, test := range tests {
59 | t.Run(test.name, func(t *testing.T) {
60 | req, err := mocks.Req(ctx, http.MethodGet, string(test.arg), nil)
61 | r, err := httpClient.Do(req)
62 | if err != nil {
63 | t.Fatalf("Err received: %+v", err)
64 | }
65 |
66 | if r.StatusCode != test.wanted.status {
67 | t.Errorf("Invalid status received %d, expected %d", r.StatusCode, http.StatusOK)
68 | }
69 | })
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/integration/main_test.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "os"
8 | "testing"
9 | )
10 |
11 | var Verbose bool
12 | var Build bool
13 |
14 | func TestMain(m *testing.M) {
15 | flag.BoolVar(&Verbose, "verbose", false, "enable more verbose logging")
16 | flag.BoolVar(&Build, "build", false, "build images before run")
17 | flag.Parse()
18 |
19 | if Build {
20 | name, err := buildImage(context.Background(), Verbose)
21 | if err != nil {
22 | _, _ = fmt.Fprintf(os.Stderr, "error building: %s", err)
23 | os.Exit(-1)
24 | }
25 | _, _ = fmt.Fprintf(os.Stdout, "built image: %s", name)
26 | }
27 |
28 | if st := m.Run(); st != 0 {
29 | os.Exit(st)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/integration/mocks/.env:
--------------------------------------------------------------------------------
1 | STORAGE_PATH=/storage
2 | STORAGE=fs
3 |
--------------------------------------------------------------------------------
/integration/mocks/import.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "https://fedbox/objects/0",
4 | "name": "#sysop",
5 | "attributedTo": "https://fedbox",
6 | "to": ["https://www.w3.org/ns/activitystreams#Public"]
7 | },
8 | {
9 | "@context": "https://www.w3.org/ns/activitystreams",
10 | "id": "https://fedbox/actors/1",
11 | "type": "Person",
12 | "attributedTo": "https://fedbox",
13 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
14 | "generator": "https://fedbox",
15 | "url": "https://fedbox/actors/1",
16 | "inbox": "https://fedbox/actors/1/inbox",
17 | "outbox": "https://fedbox/actors/1/outbox",
18 | "preferredUsername": "admin",
19 | "tag": [
20 | {
21 | "id": "https://fedbox/objects/0",
22 | "name": "#sysop",
23 | "attributedTo": "https://fedbox",
24 | "to": ["https://www.w3.org/ns/activitystreams#Public"]
25 | }
26 | ],
27 | "endpoints": {
28 | "oauthAuthorizationEndpoint": "https://fedbox/actors/1/oauth/authorize",
29 | "oauthTokenEndpoint": "https://fedbox/actors/1/oauth/token",
30 | "sharedInbox": "https://fedbox/inbox"
31 | }
32 | },
33 | {
34 | "@context": "https://www.w3.org/ns/activitystreams",
35 | "id": "https://fedbox/objects/1",
36 | "type": "Note",
37 | "attributedTo": "https://fedbox/actors/1",
38 | "content": "Hello
FedBOX
!
\n",
39 | "mediaType": "text/html",
40 | "published": "2019-09-27T14:26:43.235793852Z",
41 | "updated": "2019-09-27T14:26:43.235793852Z",
42 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
43 | "source": {
44 | "content": "Hello `FedBOX`!",
45 | "mediaType": "text/markdown"
46 | }
47 | }
48 | ]
--------------------------------------------------------------------------------
/integration/setup.go:
--------------------------------------------------------------------------------
1 | package integration
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "io/fs"
9 | "net/http"
10 | "net/url"
11 | "os"
12 | "path/filepath"
13 | "strconv"
14 | "strings"
15 | "testing"
16 | "time"
17 |
18 | "github.com/docker/docker/pkg/stdcopy"
19 | "github.com/joho/godotenv"
20 | containers "github.com/testcontainers/testcontainers-go"
21 | "github.com/testcontainers/testcontainers-go/log"
22 | "github.com/testcontainers/testcontainers-go/wait"
23 | )
24 |
25 | type fedboxContainer struct {
26 | containers.Container
27 | }
28 |
29 | type cntrs map[string]*fedboxContainer
30 |
31 | var defaultFedBOXImageName = "localhost/fedbox/app:dev"
32 |
33 | type suite struct {
34 | name string
35 | storage string
36 | }
37 |
38 | type testLogger func(s string, args ...any)
39 |
40 | func (t testLogger) Printf(s string, args ...any) {
41 | t(s, args...)
42 | }
43 |
44 | func (t testLogger) Accept(l containers.Log) {
45 | t(string(l.Content))
46 | }
47 |
48 | func initMocks(ctx context.Context, t *testing.T, suites ...suite) (cntrs, error) {
49 | m := make(cntrs)
50 |
51 | for _, s := range suites {
52 | storage := filepath.Join(".", "mocks")
53 | env := filepath.Join(storage, ".env")
54 |
55 | img := defaultFedBOXImageName
56 | if s.storage != "" {
57 | img += "-" + s.storage
58 | }
59 | c, err := Run(ctx, t, img, WithEnvFile(env), WithStorage(storage))
60 | if err != nil {
61 | return nil, fmt.Errorf("unable to initialize container %s: %w", s.name, err)
62 | }
63 | _, err = c.Inspect(ctx)
64 | if err != nil {
65 | return nil, fmt.Errorf("unable to inspect container %s: %w", c.Container, err)
66 | }
67 | m[s.name] = c
68 | }
69 |
70 | return m, nil
71 | }
72 |
73 | func (m cntrs) cleanup(t *testing.T) {
74 | for _, mm := range m {
75 | containers.CleanupContainer(t, mm)
76 | }
77 | }
78 |
79 | func (m cntrs) Req(ctx context.Context, met, u string, body io.Reader) (*http.Request, error) {
80 | uu, err := url.Parse(u)
81 | if err != nil {
82 | return nil, fmt.Errorf("received invalid url: %w", err)
83 | }
84 |
85 | fc, ok := m[uu.Host]
86 | if !ok {
87 | return nil, fmt.Errorf("no matching mock instance for the url: %s", u)
88 | }
89 |
90 | return fc.Req(ctx, met, u, body)
91 | }
92 |
93 | func (fc *fedboxContainer) Req(ctx context.Context, met, u string, body io.Reader) (*http.Request, error) {
94 | uu, err := url.Parse(u)
95 | if err != nil {
96 | return nil, fmt.Errorf("received invalid url: %w", err)
97 | }
98 |
99 | host, err := fc.Endpoint(ctx, "https")
100 | if err != nil {
101 | return nil, fmt.Errorf("unable to compose container end-point: %w", err)
102 | }
103 | uh, err := url.Parse(host)
104 | if err != nil {
105 | return nil, fmt.Errorf("invalid container url: %w", err)
106 | }
107 |
108 | origHost := uu.Host
109 | uu.Host = uh.Host
110 |
111 | u = uu.String()
112 |
113 | r, err := http.NewRequestWithContext(ctx, met, u, body)
114 | if err != nil {
115 | return nil, fmt.Errorf("unable to create request: %w", err)
116 | }
117 | r.Host = origHost
118 |
119 | return r, nil
120 | }
121 |
122 | // Run creates an instance of the FedBOX container type
123 | func Run(ctx context.Context, t testing.TB, image string, opts ...containers.ContainerCustomizer) (*fedboxContainer, error) {
124 | logger := testLogger(t.Logf)
125 |
126 | gcr := containers.GenericContainerRequest{
127 | ContainerRequest: containers.ContainerRequest{
128 | Image: image,
129 | LogConsumerCfg: &containers.LogConsumerConfig{
130 | Opts: []containers.LogProductionOption{containers.WithLogProductionTimeout(5 * time.Second)},
131 | Consumers: []containers.LogConsumer{logger},
132 | },
133 | WaitingFor: wait.ForLog("Starting").WithStartupTimeout(time.Second),
134 | },
135 | ProviderType: containers.ProviderPodman,
136 | Started: true,
137 | }
138 |
139 | opts = append(opts, WithLogger(logger))
140 | for _, opt := range opts {
141 | if err := opt.Customize(&gcr); err != nil {
142 | return nil, err
143 | }
144 | }
145 |
146 | fc, err := containers.GenericContainer(ctx, gcr)
147 | if err != nil {
148 | return nil, err
149 | }
150 | f := fedboxContainer{Container: fc}
151 |
152 | if err = f.Start(ctx); err != nil {
153 | return &f, err
154 | }
155 |
156 | initializers := [][]string{
157 | {"fedboxctl", "--env", "dev", "bootstrap"},
158 | {"fedboxctl", "--env", "dev", "pub", "import", "/storage/import.json"},
159 | }
160 | errs := make([]error, 0)
161 | for _, cmd := range initializers {
162 | st, out, err := f.Exec(ctx, cmd)
163 | if err != nil {
164 | errs = append(errs, err)
165 | }
166 | if st != 0 {
167 | // command didn't return success.
168 | errs = append(errs, fmt.Errorf("command failed"))
169 | }
170 |
171 | if _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, out); err != nil {
172 | errs = append(errs, err)
173 | }
174 | time.Sleep(100 * time.Millisecond)
175 | }
176 | return &f, errors.Join(errs...)
177 | }
178 |
179 | var envKeys = []string{
180 | "DISABLE_STORAGE_CACHE", "DISABLE_REQUEST_CACHE", "DISABLE_STORAGE_INDEX", "DISABLE_MASTODON_COMPATIBILITY",
181 | "STORAGE_PATH", "DISABLE_CACHE", "DB_PASSWORD", "LISTEN", "DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "STORAGE",
182 | "LOG_LEVEL", "TIME_OUT", "LOG_OUTPUT", "HOSTNAME", "HTTPS", "CERT_PATH", "KEY_PATH", "ENV",
183 | }
184 |
185 | func loadEnv() map[string]string {
186 | conf := make(map[string]string)
187 | for _, k := range envKeys {
188 | v := os.Getenv(k)
189 | if v == "" {
190 | continue
191 | }
192 | conf[k] = v
193 | }
194 | return conf
195 | }
196 |
197 | var defaultPort = 6669
198 |
199 | func parseListen(s string) (string, int) {
200 | pieces := strings.Split(s, ":")
201 | port := defaultPort
202 | host := ""
203 | switch len(pieces) {
204 | case 1:
205 | if p, err := strconv.Atoi(pieces[0]); err == nil {
206 | port = p
207 | }
208 | case 2:
209 | if p, err := strconv.Atoi(pieces[1]); err == nil {
210 | port = p
211 | }
212 | host = pieces[0]
213 | }
214 | return host, port
215 | }
216 |
217 | func WithLogger(logFn log.Logger) containers.CustomizeRequestOption {
218 | return func(req *containers.GenericContainerRequest) error {
219 | req.Logger = logFn
220 | return nil
221 | }
222 | }
223 |
224 | func WithEnvFile(configFile string) containers.CustomizeRequestOption {
225 | _ = godotenv.Load(configFile)
226 | return func(req *containers.GenericContainerRequest) error {
227 | if req.Env == nil {
228 | req.Env = make(map[string]string)
229 | }
230 | for k, v := range loadEnv() {
231 | if v != "" {
232 | req.Env[k] = v
233 | }
234 | }
235 | if listen, ok := req.Env["LISTEN"]; ok {
236 | host, port := parseListen(listen)
237 | req.ContainerRequest.ExposedPorts = append(req.ContainerRequest.ExposedPorts, strconv.FormatInt(int64(port), 10))
238 | req.NetworkAliases = map[string][]string{host: {host}}
239 | }
240 | if host, ok := req.Env["HOSTNAME"]; ok {
241 | req.NetworkAliases = map[string][]string{host: {host}}
242 | }
243 | return nil
244 | }
245 | }
246 |
247 | func WithStorage(storage string) containers.CustomizeRequestOption {
248 | var files []containers.ContainerFile
249 |
250 | _ = filepath.WalkDir(storage, func(path string, d fs.DirEntry, err error) error {
251 | if strings.HasPrefix(filepath.Base(path), ".") {
252 | return nil
253 | }
254 | cf := containers.ContainerFile{
255 | HostFilePath: path,
256 | ContainerFilePath: filepath.Join("/storage", strings.ReplaceAll(path, "mocks", "")),
257 | FileMode: 0o755,
258 | }
259 | files = append(files, cf)
260 | return nil
261 | })
262 |
263 | return func(req *containers.GenericContainerRequest) error {
264 | req.Files = append(req.Files, files...)
265 | return nil
266 | }
267 | }
268 |
--------------------------------------------------------------------------------
/internal/assets/assets.go:
--------------------------------------------------------------------------------
1 | //go:build !(prod || qa)
2 |
3 | package assets
4 |
5 | import "os"
6 |
7 | const TemplatesPath = "."
8 |
9 | var Templates = os.DirFS("./internal/assets/templates")
10 |
--------------------------------------------------------------------------------
/internal/assets/assets_prod.go:
--------------------------------------------------------------------------------
1 | //go:build prod || qa
2 |
3 | package assets
4 |
5 | import "embed"
6 |
7 | const TemplatesPath = "templates"
8 |
9 | //go:embed templates/*
10 | var Templates embed.FS
11 |
--------------------------------------------------------------------------------
/internal/assets/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Error
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Errors:
14 | {{ $errs := HTTPErrors . }}
15 |
16 | {{range $i, $err := $errs }}
17 | - {{$err.Message}}
18 | {{ if $err.Trace }}
19 |
20 | {{if $err.Location}}Location : {{$err.Location}}{{else}}Details{{end}}
21 | {{ range $line := $err.Trace}}
22 | {{$line.File}}:{{$line.Line}} {{$line.Callee}}
23 | {{end}}
24 |
25 | {{end}}
26 |
27 | {{end}}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/internal/assets/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{.Title}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{- $handle := .Handle -}}
13 |
14 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/internal/assets/templates/password.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{.Title}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/internal/cmd/accounts.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "time"
7 |
8 | vocab "github.com/go-ap/activitypub"
9 | "github.com/go-ap/auth"
10 | "github.com/go-ap/errors"
11 | "github.com/go-ap/fedbox"
12 | "github.com/go-ap/fedbox/storage"
13 | "github.com/go-ap/jsonld"
14 | "github.com/urfave/cli/v2"
15 | )
16 |
17 | var AccountsCmd = &cli.Command{
18 | Name: "accounts",
19 | Usage: "Accounts helper",
20 | Subcommands: []*cli.Command{
21 | exportAccountsMetadataCmd,
22 | importAccountsMetadataCmd,
23 | generateKeysCmd,
24 | },
25 | }
26 |
27 | var exportAccountsMetadataCmd = &cli.Command{
28 | Name: "export",
29 | Usage: "Exports accounts metadata",
30 | Action: exportAccountsMetadata(&ctl),
31 | }
32 |
33 | func exportAccountsMetadata(ctl *Control) cli.ActionFunc {
34 | return func(c *cli.Context) error {
35 | if err := ctl.Storage.Open(); err != nil {
36 | return errors.Annotatef(err, "Unable to open FedBOX storage for path %s", ctl.Conf.StoragePath)
37 | }
38 | defer ctl.Storage.Close()
39 |
40 | metaLoader, ok := ctl.Storage.(storage.MetadataTyper)
41 | if !ok {
42 | return errors.Newf("")
43 | }
44 |
45 | iri := SearchActorsIRI(vocab.IRI(ctl.Conf.BaseURL), ByType(vocab.PersonType))
46 | col, err := ctl.Storage.Load(iri)
47 | if err != nil {
48 | return err
49 | }
50 |
51 | items := make(vocab.ItemCollection, 0)
52 | if col.IsCollection() {
53 | err = vocab.OnCollectionIntf(col, func(c vocab.CollectionInterface) error {
54 | items = append(items, c.Collection()...)
55 | return nil
56 | })
57 | if err != nil {
58 | return err
59 | }
60 | } else {
61 | items = append(items, col)
62 | }
63 |
64 | allMeta := make(map[vocab.IRI]auth.Metadata, len(items))
65 | for _, it := range items {
66 | if it.GetType() != vocab.PersonType {
67 | continue
68 | }
69 | m, err := metaLoader.LoadMetadata(it.GetLink())
70 | if err != nil {
71 | //Errf("Error loading metadata for %s: %s", it.GetLink(), err)
72 | continue
73 | }
74 | if m == nil {
75 | //Errf("Error loading metadata for %s, nil metadata", it.GetLink())
76 | continue
77 | }
78 | allMeta[it.GetLink()] = *m
79 | }
80 | bytes, err := jsonld.Marshal(allMeta)
81 | if err != nil {
82 | return err
83 | }
84 | fmt.Printf("%s\n", bytes)
85 | return nil
86 | }
87 | }
88 |
89 | var importAccountsMetadataCmd = &cli.Command{
90 | Name: "import",
91 | Usage: "Imports accounts metadata",
92 | Action: importAccountsMetadata(&ctl),
93 | }
94 |
95 | func importAccountsMetadata(ctl *Control) cli.ActionFunc {
96 | return func(c *cli.Context) error {
97 | if err := ctl.Storage.Open(); err != nil {
98 | return errors.Annotatef(err, "Unable to open FedBOX storage for path %s", ctl.Conf.StoragePath)
99 | }
100 | defer ctl.Storage.Close()
101 |
102 | files := c.Args().Slice()
103 | metaLoader, ok := ctl.Storage.(storage.MetadataTyper)
104 | if !ok {
105 | return errors.Newf("")
106 | }
107 | for _, name := range files {
108 | f, err := os.Open(name)
109 | if err != nil {
110 | if os.IsNotExist(err) {
111 | Errf("Invalid path %s", name)
112 | } else {
113 | Errf("Error %s", err)
114 | }
115 | continue
116 | }
117 |
118 | s, err := f.Stat()
119 | if err != nil {
120 | Errf("Error %s", err)
121 | continue
122 | }
123 | buf := make([]byte, s.Size())
124 | size, err := f.Read(buf)
125 | if err != nil {
126 | Errf("Error %s", err)
127 | continue
128 | }
129 | if size == 0 {
130 | Errf("Empty file %s", name)
131 | continue
132 | }
133 |
134 | metadata := make(map[vocab.IRI]auth.Metadata, 0)
135 | err = jsonld.Unmarshal(buf, &metadata)
136 | if err != nil {
137 | Errf("Error unmarshaling JSON: %s", err)
138 | continue
139 | }
140 | start := time.Now()
141 | count := 0
142 | for iri, m := range metadata {
143 | if err = metaLoader.SaveMetadata(m, iri); err != nil {
144 | _, _ = fmt.Fprintf(os.Stderr, "unable to save metadata for %s: %s", iri, err)
145 | continue
146 | }
147 | count++
148 | }
149 |
150 | tot := time.Now().Sub(start)
151 | fmt.Printf("Elapsed time: %s\n", tot)
152 | if count > 0 {
153 | perIt := time.Duration(int64(tot) / int64(count))
154 | fmt.Printf("Elapsed time per item: %s\n", perIt)
155 | }
156 | }
157 | return nil
158 | }
159 | }
160 |
161 | var generateKeysCmd = &cli.Command{
162 | Name: "gen-keys",
163 | Usage: "Generate public/private key pairs for actors that are missing them",
164 | Flags: []cli.Flag{
165 | &cli.StringFlag{
166 | Name: "key-type",
167 | Usage: fmt.Sprintf("Type of keys to generate: %v", []string{fedbox.KeyTypeED25519, fedbox.KeyTypeRSA}),
168 | Value: fedbox.KeyTypeED25519,
169 | },
170 | },
171 | ArgsUsage: "IRI...",
172 | Action: generateKeys(&ctl),
173 | }
174 |
175 | func AddKeyToItem(metaSaver storage.MetadataTyper, it vocab.Item, typ string) error {
176 | return fedbox.AddKeyToItem(metaSaver, it, typ)
177 | }
178 |
179 | func generateKeys(ctl *Control) cli.ActionFunc {
180 | return func(c *cli.Context) error {
181 | if err := ctl.Storage.Open(); err != nil {
182 | return errors.Annotatef(err, "Unable to open FedBOX storage for path %s", ctl.Conf.StoragePath)
183 | }
184 | defer ctl.Storage.Close()
185 |
186 | typ := c.String("key-type")
187 | metaSaver, ok := ctl.Storage.(storage.MetadataTyper)
188 | if !ok {
189 | return errors.Newf("storage doesn't support saving key")
190 | }
191 |
192 | col := make(vocab.ItemCollection, 0)
193 | for i := 0; i < c.Args().Len(); i++ {
194 | iri := c.Args().Get(i)
195 | actors, err := ctl.Storage.Load(vocab.IRI(iri))
196 | if err != nil {
197 | Errf(err.Error())
198 | continue
199 | }
200 | _ = vocab.OnActor(actors, func(act *vocab.Actor) error {
201 | col = append(col, act)
202 | return nil
203 | })
204 | }
205 |
206 | if c.Args().Len() == 0 {
207 | // TODO(marius): we should improve this with filtering based on public key existing in the actor,
208 | // and with batching.
209 | iri := SearchActorsIRI(vocab.IRI(ctl.Conf.BaseURL), ByType(vocab.PersonType))
210 | actors, err := ctl.Storage.Load(iri)
211 | if err != nil {
212 | return err
213 | }
214 | _ = vocab.OnActor(actors, func(act *vocab.Actor) error {
215 | col = append(col, act)
216 | return nil
217 | })
218 | }
219 |
220 | for _, it := range col {
221 | if !vocab.ActorTypes.Contains(it.GetType()) {
222 | continue
223 | }
224 | if err := AddKeyToItem(metaSaver, it, typ); err != nil {
225 | Errf("Error: %s", err.Error())
226 | }
227 | }
228 | return nil
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/internal/cmd/bootstrap.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "git.sr.ht/~mariusor/lw"
8 | vocab "github.com/go-ap/activitypub"
9 | http "github.com/go-ap/errors"
10 | "github.com/go-ap/fedbox"
11 | ap "github.com/go-ap/fedbox/activitypub"
12 | "github.com/go-ap/fedbox/internal/config"
13 | "github.com/go-ap/fedbox/internal/storage"
14 | s "github.com/go-ap/fedbox/storage"
15 | "github.com/urfave/cli/v2"
16 | )
17 |
18 | var BootstrapCmd = &cli.Command{
19 | Name: "bootstrap",
20 | Usage: "Bootstrap a new postgres or bolt database helper",
21 | Flags: []cli.Flag{
22 | &cli.StringFlag{
23 | Name: "root",
24 | Usage: "root account of postgres server (default: postgres)",
25 | Value: "postgres",
26 | },
27 | &cli.StringFlag{
28 | Name: "sql",
29 | Usage: "path to the queries for initializing the database",
30 | Value: "postgres",
31 | },
32 | &cli.StringFlag{
33 | Name: "key-type",
34 | Usage: fmt.Sprintf("Type of keys to generate: %v", []string{fedbox.KeyTypeED25519, fedbox.KeyTypeRSA}),
35 | Value: fedbox.KeyTypeED25519,
36 | },
37 | },
38 | Action: bootstrapAct(&ctl),
39 | Subcommands: []*cli.Command{reset},
40 | }
41 |
42 | var reset = &cli.Command{
43 | Name: "reset",
44 | Usage: "reset an existing database",
45 | Action: resetAct(&ctl),
46 | }
47 |
48 | func resetAct(ctl *Control) cli.ActionFunc {
49 | return func(ctx *cli.Context) error {
50 | if err := ctl.Storage.Open(); err != nil {
51 | return http.Annotatef(err, "Unable to open FedBOX storage for path %s", ctl.Conf.StoragePath)
52 | }
53 | defer ctl.Storage.Close()
54 |
55 | err := Reset(ctl.Conf)
56 | if err != nil {
57 | return err
58 | }
59 | return Bootstrap(ctl.Conf, ctl.Service)
60 | }
61 | }
62 |
63 | func bootstrapAct(ctl *Control) cli.ActionFunc {
64 | return func(ctx *cli.Context) error {
65 | if err := ctl.Storage.Open(); err != nil {
66 | return http.Annotatef(err, "Unable to open FedBOX storage for path %s", ctl.Conf.StoragePath)
67 | }
68 | defer ctl.Storage.Close()
69 |
70 | keyType := ctx.String("keyType")
71 | ctl.Service = ap.Self(ap.DefaultServiceIRI(ctl.Conf.BaseURL))
72 | if err := Bootstrap(ctl.Conf, ctl.Service); err != nil {
73 | Errf("Error adding service: %s\n", err)
74 | return err
75 | }
76 | if metaSaver, ok := ctl.Storage.(s.MetadataTyper); ok {
77 | if err := AddKeyToItem(metaSaver, &ctl.Service, keyType); err != nil {
78 | Errf("Error saving metadata for service: %s", err)
79 | return err
80 | }
81 | }
82 | return nil
83 | }
84 | }
85 |
86 | func Bootstrap(conf config.Options, service vocab.Item) error {
87 | l := lw.Prod(lw.SetLevel(conf.LogLevel), lw.SetOutput(os.Stdout))
88 | if err := storage.BootstrapFn(conf); err != nil {
89 | return http.Annotatef(err, "Unable to create %s path for storage %s", conf.BaseStoragePath(), conf.Storage)
90 | }
91 | l.Infof("Successfully created %s db for storage %s", conf.BaseStoragePath(), conf.Storage)
92 |
93 | db, err := fedbox.Storage(conf, l)
94 | if err != nil {
95 | return http.Annotatef(err, "Unable to initialize FedBOX storage for path %s", conf.StoragePath)
96 | }
97 | if err := db.Open(); err != nil {
98 | return http.Annotatef(err, "Unable to open FedBOX storage for path %s", conf.StoragePath)
99 | }
100 | defer db.Close()
101 |
102 | if err = CreateService(db, service); err != nil {
103 | return http.Annotatef(err, "Unable to create FedBOX service %s for storage %s", service.GetID(), conf.Storage)
104 | }
105 | l.Infof("Successfully created FedBOX service %s for storage %s", service.GetID(), conf.Storage)
106 | return nil
107 | }
108 |
109 | func Reset(conf config.Options) error {
110 | l := lw.Prod(lw.SetLevel(conf.LogLevel), lw.SetOutput(os.Stdout))
111 | if err := storage.CleanFn(conf); err != nil {
112 | return http.Annotatef(err, "Unable to reset %s db for storage %s", conf.BaseStoragePath(), conf.Storage)
113 | }
114 | l.Infof("Successfully reset %s db for storage %s", conf.BaseStoragePath(), conf.Storage)
115 | return nil
116 | }
117 |
118 | func CreateService(r s.FullStorage, self vocab.Item) (err error) {
119 | return fedbox.CreateService(r, self)
120 | }
121 |
--------------------------------------------------------------------------------
/internal/cmd/control.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "os"
8 | "time"
9 |
10 | "git.sr.ht/~mariusor/lw"
11 | vocab "github.com/go-ap/activitypub"
12 | "github.com/go-ap/errors"
13 | "github.com/go-ap/fedbox"
14 | ap "github.com/go-ap/fedbox/activitypub"
15 | "github.com/go-ap/fedbox/internal/config"
16 | "github.com/go-ap/fedbox/internal/env"
17 | st "github.com/go-ap/fedbox/storage"
18 | "github.com/urfave/cli/v2"
19 | "golang.org/x/crypto/ssh/terminal"
20 | )
21 |
22 | type Control struct {
23 | Conf config.Options
24 | Logger lw.Logger
25 | Service vocab.Actor
26 | Storage st.FullStorage
27 | }
28 |
29 | func New(db st.FullStorage, conf config.Options, l lw.Logger) (*Control, error) {
30 | self, err := ap.LoadActor(db, ap.DefaultServiceIRI(conf.BaseURL))
31 | if err != nil {
32 | l.Warnf("unable to load actor: %s", err)
33 | }
34 |
35 | return &Control{
36 | Conf: conf,
37 | Service: self,
38 | Storage: db,
39 | Logger: l,
40 | }, nil
41 | }
42 |
43 | var ctl Control
44 |
45 | func Before(c *cli.Context) error {
46 | fields := lw.Ctx{}
47 |
48 | logLevel := lw.InfoLevel
49 | if c.Bool("verbose") {
50 | logLevel = lw.DebugLevel
51 | }
52 | logger := lw.Prod(lw.SetLevel(logLevel), lw.SetOutput(os.Stdout))
53 | ct, err := setup(c, logger.WithContext(fields))
54 | if err != nil {
55 | // Ensure we don't print the default help message, which is not useful here
56 | c.App.CustomAppHelpTemplate = "Failed"
57 | logger.WithContext(lw.Ctx{"err": err}).Errorf("Error")
58 | return err
59 | }
60 | ctl = *ct
61 |
62 | return nil
63 | }
64 |
65 | func setup(c *cli.Context, l lw.Logger) (*Control, error) {
66 | environ := env.Type(c.String("env"))
67 | conf, err := config.Load(environ, time.Second)
68 | if err != nil {
69 | l.Errorf("Unable to load config files for environment %s: %s", environ, err)
70 | }
71 | path := c.String("path")
72 | if path != "." {
73 | conf.StoragePath = path
74 | }
75 | typ := c.String("type")
76 | if typ != "" {
77 | conf.Storage = config.StorageType(typ)
78 | }
79 | if conf.Storage == config.StoragePostgres {
80 | host := c.String("host")
81 | if host == "" {
82 | host = "localhost"
83 | }
84 | port := c.Int64("port")
85 | if port == 0 {
86 | host = path
87 | }
88 | user := c.String("user")
89 | if user == "" {
90 | user = "fedbox"
91 | }
92 | pw, err := loadPwFromStdin(true, "%s@%s's", user, host)
93 | if err != nil {
94 | return nil, err
95 | }
96 | _ = config.BackendConfig{
97 | Enabled: false,
98 | Host: host,
99 | Port: port,
100 | User: user,
101 | Pw: string(pw),
102 | Name: user,
103 | }
104 | }
105 | db, err := fedbox.Storage(conf, l)
106 | if err != nil {
107 | return nil, err
108 | }
109 | if err = db.Open(); err != nil {
110 | return nil, errors.Annotatef(err, "Unable to open FedBOX storage for path %s", conf.StoragePath)
111 | }
112 | defer db.Close()
113 |
114 | return New(db, conf, l)
115 | }
116 |
117 | func loadPwFromStdin(confirm bool, s string, params ...any) ([]byte, error) {
118 | fmt.Printf(s+" pw: ", params...)
119 | pw1, _ := terminal.ReadPassword(0)
120 | fmt.Println()
121 | if confirm {
122 | fmt.Printf("pw again: ")
123 | pw2, _ := terminal.ReadPassword(0)
124 | fmt.Println()
125 | if !bytes.Equal(pw1, pw2) {
126 | return nil, errors.Errorf("Passwords do not match")
127 | }
128 | }
129 | return pw1, nil
130 | }
131 |
132 | func loadFromStdin(s string, params ...any) ([]byte, error) {
133 | reader := bufio.NewReader(os.Stdin)
134 | fmt.Printf(s+": ", params...)
135 | input, _ := reader.ReadBytes('\n')
136 | return input[:len(input)-1], nil
137 | }
138 |
139 | func Errf(s string, par ...any) {
140 | _, _ = fmt.Fprintf(os.Stderr, s+"\n", par...)
141 | }
142 |
--------------------------------------------------------------------------------
/internal/cmd/fedbox.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "os"
8 |
9 | "git.sr.ht/~mariusor/lw"
10 | "github.com/go-ap/errors"
11 | "github.com/go-ap/fedbox"
12 | "github.com/go-ap/fedbox/internal/config"
13 | "github.com/go-ap/fedbox/internal/env"
14 | "github.com/urfave/cli/v2"
15 | )
16 |
17 | const AppName = "FedBOX"
18 |
19 | func NewApp(version string) *cli.App {
20 | return &cli.App{
21 | Name: AppName,
22 | Description: AppName + " instance server",
23 | Version: version,
24 | Flags: []cli.Flag{
25 | &cli.DurationFlag{
26 | Name: "wait",
27 | Usage: "the duration for which the server gracefully wait for existing connections to finish",
28 | },
29 | &cli.StringFlag{
30 | Name: "env",
31 | Usage: fmt.Sprintf("the environment to use. Possible values: %q, %q, %q", env.DEV, env.QA, env.PROD),
32 | Value: string(env.DEV),
33 | },
34 | &cli.BoolFlag{
35 | Name: "profile",
36 | Hidden: true,
37 | Value: false,
38 | },
39 | },
40 | Action: run(version),
41 | }
42 | }
43 |
44 | func run(version string) cli.ActionFunc {
45 | return func(c *cli.Context) error {
46 | w := c.Duration("wait")
47 | e := c.String("env")
48 |
49 | conf, err := config.Load(env.Type(e), w)
50 | conf.Profile = c.Bool("profile")
51 | conf.Secure = conf.Secure && !conf.Profile
52 | conf.AppName = c.App.Name
53 | conf.Version = version
54 |
55 | if err != nil {
56 | return err
57 | }
58 | var out io.WriteCloser
59 | if conf.LogOutput != "" {
60 | if out, err = os.Open(conf.LogOutput); err != nil {
61 | return errors.Newf("Unable to output logs to %s: %s", conf.LogOutput, err)
62 | }
63 | defer func() {
64 | if err := out.Close(); err != nil {
65 | _, _ = fmt.Fprintf(os.Stderr, "Unable to close log output: %s", err)
66 | }
67 | }()
68 | }
69 | var l lw.Logger
70 | if conf.Env.IsDev() {
71 | l = lw.Dev(lw.SetLevel(conf.LogLevel), lw.SetOutput(out))
72 | } else {
73 | l = lw.Prod(lw.SetLevel(conf.LogLevel), lw.SetOutput(out))
74 | }
75 | db, err := fedbox.Storage(conf, l.WithContext(lw.Ctx{"log": "storage"}))
76 |
77 | a, err := fedbox.New(l.WithContext(lw.Ctx{"log": "fedbox"}), conf, db)
78 | if err != nil {
79 | l.Errorf("Unable to initialize: %s", err)
80 | return err
81 | }
82 |
83 | return a.Run(context.Background())
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/internal/cmd/filters.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | vocab "github.com/go-ap/activitypub"
5 | "github.com/go-ap/filters"
6 | )
7 |
8 | func types(incoming ...string) filters.Check {
9 | validTypes := make(vocab.ActivityVocabularyTypes, 0, len(incoming))
10 | for _, t := range incoming {
11 | if tt := vocab.ActivityVocabularyType(t); vocab.Types.Contains(tt) {
12 | validTypes = append(validTypes)
13 | }
14 | }
15 | return filters.HasType(validTypes...)
16 | }
17 |
18 | func names(incoming ...string) filters.Check {
19 | checks := make(filters.Checks, 0, len(incoming))
20 | for _, t := range incoming {
21 | checks = append(checks, filters.NameIs(t))
22 | }
23 | return filters.Any(checks...)
24 | }
25 |
--------------------------------------------------------------------------------
/internal/cmd/fix-storage-collections.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | vocab "github.com/go-ap/activitypub"
5 | "github.com/go-ap/errors"
6 | st "github.com/go-ap/fedbox/storage"
7 | "github.com/go-ap/filters"
8 | "github.com/go-ap/processing"
9 | "github.com/urfave/cli/v2"
10 | "time"
11 | )
12 |
13 | var FixStorageCollectionsCmd = &cli.Command{
14 | Name: "fix-storage",
15 | Usage: "Fix storage collections helper",
16 | Action: fixStorageCollectionsAct(&ctl),
17 | }
18 |
19 | var allCollectionPaths = append(filters.FedBOXCollections, vocab.ActivityPubCollections...)
20 | var streamCollections = vocab.CollectionPaths{
21 | filters.ActivitiesType,
22 | filters.ActorsType,
23 | filters.ObjectsType,
24 | }
25 |
26 | func newOrderedCollection(id vocab.IRI) *vocab.OrderedCollection {
27 | return &vocab.OrderedCollection{
28 | ID: id,
29 | Type: vocab.OrderedCollectionType,
30 | Generator: ctl.Service.GetLink(),
31 | Published: time.Now().UTC(),
32 | }
33 | }
34 |
35 | func getObjectCollections(act vocab.Item) vocab.IRIs {
36 | collections := make(vocab.IRIs, 0)
37 | for _, col := range vocab.OfObject {
38 | if colIRI := col.IRI(act); colIRI != "" {
39 | collections = append(collections, colIRI)
40 | }
41 | }
42 | return collections
43 | }
44 |
45 | func getActorCollections(act vocab.Item) vocab.IRIs {
46 | collections := make(vocab.IRIs, 0)
47 | for _, col := range vocab.OfActor {
48 | if colIRI := col.IRI(act); colIRI != "" {
49 | collections = append(collections, colIRI)
50 | }
51 | }
52 | return collections
53 | }
54 |
55 | func fixStorageCollectionsAct(ctl *Control) cli.ActionFunc {
56 | return func(c *cli.Context) error {
57 | if _, ok := ctl.Storage.(processing.CollectionStore); !ok {
58 | return errors.Newf("Invalid storage type %T. Unable to handle collection operations.", ctl.Storage)
59 | }
60 | if err := ctl.Storage.Open(); err != nil {
61 | return errors.Annotatef(err, "Unable to open FedBOX storage for path %s", ctl.Conf.StoragePath)
62 | }
63 | defer ctl.Storage.Close()
64 |
65 | if err := tryCreateActorCollections(ctl.Service, ctl.Storage); err != nil {
66 | return err
67 | }
68 | // NOTE(marius): this assumes that storage contains the actors, activities, objects streams collections
69 | return tryCreateAllObjectsCollections(ctl.Service, ctl.Storage)
70 | }
71 | }
72 |
73 | func tryCreateAllObjectsCollections(actor vocab.Item, storage st.FullStorage) error {
74 | if actor == nil {
75 | return nil
76 | }
77 |
78 | allCollections := make(vocab.IRIs, 0)
79 | err := vocab.OnActor(actor, func(actor *vocab.Actor) error {
80 | if actor.Streams == nil {
81 | return nil
82 | }
83 | for _, stream := range actor.Streams {
84 | _, maybeCol := allCollectionPaths.Split(stream.GetLink())
85 | if !streamCollections.Contains(maybeCol) {
86 | continue
87 | }
88 | iri := maybeCol.IRI(ctl.Service)
89 | items, err := ctl.Storage.Load(iri)
90 | if err != nil {
91 | ctl.Logger.Debugf("Unable to load collection %s: %s", iri, err)
92 | continue
93 | }
94 | vocab.OnCollectionIntf(items, func(col vocab.CollectionInterface) error {
95 | for _, it := range col.Collection() {
96 | if vocab.ActorTypes.Contains(it.GetType()) {
97 | allCollections = append(allCollections, getActorCollections(it)...)
98 | } else {
99 | if it.IsCollection() {
100 | continue
101 | }
102 | allCollections = append(allCollections, getObjectCollections(it)...)
103 | }
104 | }
105 | return nil
106 | })
107 | }
108 | return nil
109 | })
110 | if err != nil {
111 | return err
112 | }
113 | for _, col := range allCollections {
114 | if err := tryCreateCollection(storage, col); err != nil {
115 | ctl.Logger.Warnf("Error when trying to create collection: %+s", err)
116 | continue
117 | }
118 | }
119 | return nil
120 | }
121 |
122 | func tryCreateActorCollections(actor vocab.Item, storage st.FullStorage) error {
123 | initialCollections := make([]vocab.IRI, 0)
124 | initialCollections = append(initialCollections, getActorCollections(actor)...)
125 | err := vocab.OnActor(actor, func(actor *vocab.Actor) error {
126 | if actor.Streams == nil {
127 | return nil
128 | }
129 | for _, stream := range actor.Streams {
130 | if _, maybeCol := allCollectionPaths.Split(stream.GetLink()); !allCollectionPaths.Contains(maybeCol) {
131 | ctl.Logger.Debugf("Stream doesn't seem to be a collection", stream)
132 | return nil
133 | }
134 | initialCollections = append(initialCollections, stream.GetLink())
135 | }
136 | return nil
137 | })
138 | if err != nil {
139 | return err
140 | }
141 | for _, col := range initialCollections {
142 | err := tryCreateCollection(storage, col)
143 | if err != nil {
144 | ctl.Logger.Warnf("Error when trying to create collection: %+s", err)
145 | continue
146 | }
147 | }
148 | return nil
149 | }
150 |
151 | func tryCreateCollection(storage st.FullStorage, colIRI vocab.IRI) error {
152 | var collection *vocab.OrderedCollection
153 | items, err := ctl.Storage.Load(colIRI.GetLink())
154 | if err != nil {
155 | if !errors.IsNotFound(err) {
156 | ctl.Logger.Errorf("Unable to load %s: %s", colIRI, err)
157 | return err
158 | }
159 | colSaver, ok := storage.(processing.CollectionStore)
160 | if !ok {
161 | return errors.Newf("Invalid storage type %T. Unable to handle collection operations.", storage)
162 | }
163 | it, err := colSaver.Create(newOrderedCollection(colIRI.GetLink()))
164 | if err != nil {
165 | ctl.Logger.Errorf("Unable to create collection %s: %s", colIRI, err)
166 | return err
167 | }
168 | collection, err = vocab.ToOrderedCollection(it)
169 | if err != nil {
170 | ctl.Logger.Errorf("Saved object is not a valid OrderedCollection, but %s: %s", it.GetType(), err)
171 | return err
172 | }
173 | }
174 |
175 | if vocab.IsNil(items) {
176 | return nil
177 | }
178 |
179 | if !items.IsCollection() {
180 | if _, err := storage.Save(items); err != nil {
181 | ctl.Logger.Errorf("Unable to save object %s: %s", items.GetLink(), err)
182 | return err
183 | }
184 | }
185 | collection, err = vocab.ToOrderedCollection(items)
186 | if err != nil {
187 | ctl.Logger.Errorf("Saved object is not a valid OrderedCollection, but %s: %s", items.GetType(), err)
188 | return err
189 | }
190 | vocab.OnCollectionIntf(items, func(col vocab.CollectionInterface) error {
191 | collection.TotalItems = col.Count()
192 | for _, it := range col.Collection() {
193 | // Try saving objects in collection, which would create the collections if they exist
194 | if _, err := storage.Save(it); err != nil {
195 | ctl.Logger.Errorf("Unable to save object %s: %s", it.GetLink(), err)
196 | }
197 | }
198 | return nil
199 | })
200 |
201 | collection.OrderedItems = nil
202 | _, err = storage.Save(collection)
203 | if err != nil {
204 | ctl.Logger.Errorf("Unable to save collection with updated totalItems", err)
205 | return err
206 | }
207 |
208 | return nil
209 | }
210 |
--------------------------------------------------------------------------------
/internal/cmd/maintenance.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "syscall"
5 |
6 | "github.com/urfave/cli/v2"
7 | )
8 |
9 | var Maintenance = &cli.Command{
10 | Name: "maintenance",
11 | Usage: "Toggle maintenance mode for the main FedBOX server",
12 | Action: sendSignalToServerAct(&ctl, syscall.SIGUSR1),
13 | }
14 |
15 | var Reload = &cli.Command{
16 | Name: "reload",
17 | Usage: "Reload the main FedBOX server configuration",
18 | Action: sendSignalToServerAct(&ctl, syscall.SIGHUP),
19 | }
20 |
21 | var Stop = &cli.Command{
22 | Name: "stop",
23 | Usage: "Stops the main FedBOX server configuration",
24 | Action: sendSignalToServerAct(&ctl, syscall.SIGTERM),
25 | }
26 |
27 | func sendSignalToServerAct(ctl *Control, sig syscall.Signal) cli.ActionFunc {
28 | return func(c *cli.Context) error {
29 | pid, err := ctl.Conf.ReadPid()
30 | if err != nil {
31 | return err
32 | }
33 | return syscall.Kill(pid, sig)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/internal/cmd/output.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "strings"
8 |
9 | vocab "github.com/go-ap/activitypub"
10 | )
11 |
12 | func bytef(s string, p ...any) []byte {
13 | return []byte(fmt.Sprintf(s, p...))
14 | }
15 |
16 | func outObject(o *vocab.Object, b io.Writer) error {
17 | _, _ = b.Write(bytef("[%s] %s // %s", o.Type, o.ID, o.Published.Format("02 Jan 2006 15:04:05")))
18 | if len(o.Name) > 0 {
19 | for _, s := range o.Name {
20 | ss := strings.Trim(s.Value.String(), "\n\r\t ")
21 | if s.Ref != vocab.NilLangRef {
22 | _, _ = b.Write(bytef("\n\tName[%s]: %s", s.Ref, ss))
23 | }
24 | _, _ = b.Write(bytef("\n\tName: %s", ss))
25 | }
26 | }
27 | if o.Summary != nil {
28 | for _, s := range o.Summary {
29 | ss := strings.Trim(s.Value.String(), "\n\r\t ")
30 | if s.Ref != vocab.NilLangRef {
31 | cont := s.Ref
32 | if len(cont) > 72 {
33 | cont = cont[:72]
34 | }
35 | _, _ = b.Write(bytef("\n\tSummary[%s]: %s", cont, ss))
36 | }
37 | _, _ = b.Write(bytef("\n\tSummary: %s", ss))
38 | }
39 | }
40 | if o.Content != nil {
41 | for _, c := range o.Content {
42 | cc := strings.Trim(c.Value.String(), "\n\r\t ")
43 | if c.Ref != vocab.NilLangRef {
44 | cont := c.Ref
45 | if len(cont) > 72 {
46 | cont = cont[:72]
47 | }
48 | _, _ = b.Write(bytef("\n\tContent[%s]: %s", cont, cc))
49 | }
50 | _, _ = b.Write(bytef("\n\tContent: %s", cc))
51 | }
52 | }
53 | return nil
54 | }
55 |
56 | func outActivity(a *vocab.Activity, b io.Writer) error {
57 | err := vocab.OnObject(a, func(o *vocab.Object) error {
58 | return outObject(o, b)
59 | })
60 | if err != nil {
61 | return err
62 | }
63 | if a.Actor != nil {
64 | b.Write(bytef("\n\tActor: "))
65 | outItem(a.Actor, b)
66 | }
67 | if a.Object != nil {
68 | b.Write(bytef("\n\tObject: "))
69 | outItem(a.Object, b)
70 | }
71 |
72 | return nil
73 | }
74 |
75 | func outActor(a *vocab.Actor, b io.Writer) error {
76 | err := vocab.OnObject(a, func(o *vocab.Object) error {
77 | return outObject(o, b)
78 | })
79 | if err != nil {
80 | return err
81 | }
82 | if len(a.PreferredUsername) > 0 {
83 | for _, s := range a.PreferredUsername {
84 | ss := strings.Trim(s.Value.String(), "\n\r\t ")
85 | if s.Ref != vocab.NilLangRef {
86 | b.Write(bytef("\n\tPreferredUsername[%s]: %s", s.Ref, ss))
87 | }
88 | b.Write(bytef("\n\tPreferredUsername: %s", ss))
89 | }
90 | }
91 | return nil
92 | }
93 | func outItem(it vocab.Item, b io.Writer) error {
94 | if it.IsCollection() {
95 | return vocab.OnCollectionIntf(it, func(c vocab.CollectionInterface) error {
96 | for _, it := range c.Collection() {
97 | outItem(it, b)
98 | b.Write([]byte("\n"))
99 | }
100 | return nil
101 | })
102 | }
103 | if it.IsLink() {
104 | _, err := b.Write([]byte(it.GetLink()))
105 | return err
106 | }
107 | typ := it.GetType()
108 | if vocab.ActivityTypes.Contains(typ) || vocab.IntransitiveActivityTypes.Contains(typ) {
109 | return vocab.OnActivity(it, func(a *vocab.Activity) error {
110 | return outActivity(a, b)
111 | })
112 | }
113 | if vocab.ActorTypes.Contains(typ) {
114 | return vocab.OnActor(it, func(a *vocab.Actor) error {
115 | return outActor(a, b)
116 | })
117 | }
118 | return vocab.OnObject(it, func(o *vocab.Object) error {
119 | return outObject(o, b)
120 | })
121 | }
122 |
123 | func outText(where io.Writer) func(it vocab.Item) error {
124 | return func(it vocab.Item) error {
125 | b := new(bytes.Buffer)
126 | err := outItem(it, b)
127 | if err != nil {
128 | return err
129 | }
130 | _, _ = fmt.Fprintf(where, "%s", b.Bytes())
131 | return nil
132 | }
133 | }
134 |
135 | func outJSON(where io.Writer) func(it vocab.Item) error {
136 | return func(it vocab.Item) error {
137 | out, err := vocab.MarshalJSON(it)
138 | if err != nil {
139 | return err
140 | }
141 | _, _ = fmt.Fprintf(where, "%s", out)
142 | return nil
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "net/url"
6 | "os"
7 | "path/filepath"
8 | "strconv"
9 | "strings"
10 | "time"
11 |
12 | "git.sr.ht/~mariusor/lw"
13 | "github.com/go-ap/errors"
14 | "github.com/go-ap/fedbox/internal/env"
15 | "github.com/joho/godotenv"
16 | )
17 |
18 | var (
19 | Prefix = "fedbox"
20 | BaseRuntimeDir = "/run"
21 | )
22 |
23 | type BackendConfig struct {
24 | Enabled bool
25 | Host string
26 | Port int64
27 | User string
28 | Pw string
29 | Name string
30 | }
31 |
32 | type Options struct {
33 | AppName string
34 | Version string
35 | Env env.Type
36 | LogLevel lw.Level
37 | LogOutput string
38 | TimeOut time.Duration
39 | Secure bool
40 | CertPath string
41 | KeyPath string
42 | Host string
43 | Listen string
44 | BaseURL string
45 | Storage StorageType
46 | StoragePath string
47 | StorageCache bool
48 | RequestCache bool
49 | UseIndex bool
50 | Profile bool
51 | MastodonCompatible bool
52 | MaintenanceMode bool
53 | ShuttingDown bool
54 | }
55 |
56 | type StorageType string
57 |
58 | const (
59 | RUNTIME_DIR = "XDG_RUNTIME_DIR"
60 |
61 | KeyENV = "ENV"
62 | KeyTimeOut = "TIME_OUT"
63 | KeyLogLevel = "LOG_LEVEL"
64 | KeyLogOutput = "LOG_OUTPUT"
65 | KeyHostname = "HOSTNAME"
66 | KeyHTTPS = "HTTPS"
67 | KeyCertPath = "CERT_PATH"
68 | KeyKeyPath = "KEY_PATH"
69 | KeyListen = "LISTEN"
70 | KeyDBHost = "DB_HOST"
71 | KeyDBPort = "DB_PORT"
72 | KeyDBName = "DB_NAME"
73 | KeyDBUser = "DB_USER"
74 | KeyDBPw = "DB_PASSWORD"
75 | KeyStorage = "STORAGE"
76 | KeyStoragePath = "STORAGE_PATH"
77 | KeyCacheDisable = "DISABLE_CACHE"
78 | KeyStorageCacheDisable = "DISABLE_STORAGE_CACHE"
79 | KeyRequestCacheDisable = "DISABLE_REQUEST_CACHE"
80 | KeyStorageIndexDisable = "DISABLE_STORAGE_INDEX"
81 | KeyMastodonCompatibilityDisable = "DISABLE_MASTODON_COMPATIBILITY"
82 |
83 | varEnv = "%env%"
84 | varStorage = "%storage%"
85 | varHost = "%host%"
86 |
87 | StorageBoltDB = StorageType("boltdb")
88 | StorageFS = StorageType("fs")
89 | StorageBadger = StorageType("badger")
90 | StoragePostgres = StorageType("postgres")
91 | StorageSqlite = StorageType("sqlite")
92 | )
93 |
94 | const defaultDirPerm = os.ModeDir | os.ModePerm | 0700
95 |
96 | func normalizeConfigPath(p string, o Options) string {
97 | if len(p) == 0 {
98 | return p
99 | }
100 | if p[0] == '~' {
101 | p = os.Getenv("HOME") + p[1:]
102 | }
103 | if !filepath.IsAbs(p) {
104 | p, _ = filepath.Abs(p)
105 | }
106 | p = strings.ReplaceAll(p, varEnv, string(o.Env))
107 | p = strings.ReplaceAll(p, varStorage, string(o.Storage))
108 | p = strings.ReplaceAll(p, varHost, url.PathEscape(o.Host))
109 | return filepath.Clean(p)
110 | }
111 |
112 | func (o Options) BaseStoragePath() string {
113 | basePath := normalizeConfigPath(o.StoragePath, o)
114 | fi, err := os.Stat(basePath)
115 | if err != nil && os.IsNotExist(err) {
116 | err = os.MkdirAll(basePath, defaultDirPerm)
117 | }
118 | if err != nil {
119 | panic(err)
120 | }
121 | fi, err = os.Stat(basePath)
122 | if err != nil {
123 | panic(err)
124 | }
125 | if !fi.IsDir() {
126 | panic(errors.NotValidf("path %s is invalid for storage", basePath))
127 | }
128 | return basePath
129 | }
130 |
131 | func prefKey(k string) string {
132 | if Prefix == "" {
133 | return k
134 | }
135 | return strings.Join([]string{strings.ToUpper(Prefix), k}, "_")
136 | }
137 |
138 | func Getval(name, def string) string {
139 | val := def
140 | if pf := os.Getenv(prefKey(name)); len(pf) > 0 {
141 | val = pf
142 | }
143 | if p := os.Getenv(name); len(p) > 0 {
144 | val = p
145 | }
146 | return val
147 | }
148 |
149 | func Load(e env.Type, timeOut time.Duration) (Options, error) {
150 | if !env.ValidType(e) {
151 | e = env.Type(Getval(KeyENV, ""))
152 | }
153 | configs := []string{
154 | ".env",
155 | }
156 | appendIfFile := func(typ env.Type) {
157 | envFile := fmt.Sprintf(".env.%s", typ)
158 | if _, err := os.Stat(envFile); err == nil {
159 | configs = append(configs, envFile)
160 | }
161 | }
162 | if !env.ValidType(e) {
163 | for _, typ := range env.Types {
164 | appendIfFile(typ)
165 | }
166 | } else {
167 | appendIfFile(e)
168 | }
169 | for _, f := range configs {
170 | _ = godotenv.Load(f)
171 | }
172 |
173 | opts := LoadFromEnv()
174 | opts.AppName = strings.Trim(Prefix, "_")
175 | opts.Env = e
176 | opts.TimeOut = timeOut
177 |
178 | return opts, nil
179 | }
180 |
181 | func LoadFromEnv() Options {
182 | conf := Options{}
183 | lvl := Getval(KeyLogLevel, "")
184 | switch strings.ToLower(lvl) {
185 | case "none":
186 | conf.LogLevel = lw.NoLevel
187 | case "trace":
188 | conf.LogLevel = lw.TraceLevel
189 | case "debug":
190 | conf.LogLevel = lw.DebugLevel
191 | case "warn":
192 | conf.LogLevel = lw.WarnLevel
193 | case "error":
194 | conf.LogLevel = lw.ErrorLevel
195 | case "info":
196 | fallthrough
197 | default:
198 | conf.LogLevel = lw.InfoLevel
199 | }
200 | conf.LogOutput = Getval(KeyLogOutput, "")
201 |
202 | conf.Env = env.Type(Getval(KeyENV, "dev"))
203 | if conf.Host == "" {
204 | conf.Host = Getval(KeyHostname, conf.Host)
205 | }
206 | conf.TimeOut = 0
207 | if to, _ := time.ParseDuration(Getval(KeyTimeOut, "")); to > 0 {
208 | conf.TimeOut = to
209 | }
210 | conf.Secure, _ = strconv.ParseBool(Getval(KeyHTTPS, "false"))
211 | if conf.Secure {
212 | conf.BaseURL = fmt.Sprintf("https://%s", conf.Host)
213 | } else {
214 | conf.BaseURL = fmt.Sprintf("http://%s", conf.Host)
215 | }
216 |
217 | conf.Listen = Getval(KeyListen, "")
218 | conf.Storage = StorageType(strings.ToLower(Getval(KeyStorage, string(DefaultStorage))))
219 | conf.StoragePath = Getval(KeyStoragePath, "")
220 | if conf.StoragePath == "" {
221 | conf.StoragePath = os.TempDir()
222 | }
223 | conf.StoragePath = filepath.Clean(conf.StoragePath)
224 |
225 | disableCache, _ := strconv.ParseBool(Getval(KeyCacheDisable, "false"))
226 | conf.StorageCache = !disableCache
227 | conf.RequestCache = !disableCache
228 |
229 | if v := Getval(KeyStorageCacheDisable, ""); v != "" {
230 | disableStorageCache, _ := strconv.ParseBool(v)
231 | conf.StorageCache = !disableStorageCache
232 | }
233 |
234 | if v := Getval(KeyRequestCacheDisable, ""); v != "" {
235 | disableRequestCache, _ := strconv.ParseBool(v)
236 | conf.RequestCache = !disableRequestCache
237 | }
238 |
239 | if v := Getval(KeyStorageIndexDisable, "false"); v != "" {
240 | disableStorageIndex, _ := strconv.ParseBool(v)
241 | conf.UseIndex = !disableStorageIndex
242 | }
243 |
244 | disableMastodonCompatibility, _ := strconv.ParseBool(Getval(KeyMastodonCompatibilityDisable, "false"))
245 | conf.MastodonCompatible = !disableMastodonCompatibility
246 |
247 | conf.KeyPath = normalizeConfigPath(Getval(KeyKeyPath, ""), conf)
248 | conf.CertPath = normalizeConfigPath(Getval(KeyCertPath, ""), conf)
249 |
250 | return conf
251 | }
252 |
253 | func (o Options) RuntimePath() string {
254 | path := BaseRuntimeDir
255 | if runtimeDir := os.Getenv(RUNTIME_DIR); runtimeDir != "" {
256 | path = runtimeDir
257 | }
258 | return path
259 | }
260 |
261 | func (o Options) DefaultSocketPath() string {
262 | name := o.pathInstanceName()
263 | return filepath.Join(o.RuntimePath(), name+".sock")
264 | }
265 |
266 | func (o Options) pathInstanceName() string {
267 | name := o.AppName
268 | if o.Host != "" {
269 | name += "-" + o.Host
270 | }
271 | return name
272 | }
273 |
274 | func (o Options) PidPath() string {
275 | name := o.pathInstanceName()
276 | return filepath.Join(o.RuntimePath(), name+".pid")
277 | }
278 |
279 | func (o Options) WritePid() error {
280 | pid := os.Getpid()
281 | raw := make([]byte, 0)
282 | raw = strconv.AppendUint(raw, uint64(pid), 10)
283 |
284 | pidPath := o.PidPath()
285 | if err := os.MkdirAll(filepath.Dir(pidPath), 0o700); err != nil {
286 | return err
287 | }
288 |
289 | return os.WriteFile(pidPath, raw, 0o600)
290 | }
291 |
292 | func (o Options) ReadPid() (int, error) {
293 | raw, err := os.ReadFile(o.PidPath())
294 | if err != nil {
295 | return -1, err
296 | }
297 |
298 | pid, err := strconv.ParseUint(string(raw), 10, 32)
299 | if err != nil {
300 | return -1, err
301 | }
302 | return int(pid), nil
303 | }
304 |
--------------------------------------------------------------------------------
/internal/config/config_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 | "testing"
8 | "time"
9 |
10 | "github.com/go-ap/fedbox/internal/env"
11 | )
12 |
13 | const (
14 | hostname = "testing.git"
15 | logLvl = "panic"
16 | secure = true
17 | listen = "127.0.0.3:666"
18 | pgSQL = "postgres"
19 | boltDB = "boltdb"
20 | dbHost = "127.0.0.6"
21 | dbPort = 54321
22 | dbName = "test"
23 | dbUser = "test"
24 | dbPw = "pw123+-098"
25 | )
26 |
27 | func TestLoadFromEnv(t *testing.T) {
28 | {
29 | t.Skipf("we're no longer loading SQL db config env variables")
30 | _ = os.Setenv(KeyDBHost, dbHost)
31 | _ = os.Setenv(KeyDBPort, fmt.Sprintf("%d", dbPort))
32 | _ = os.Setenv(KeyDBName, dbName)
33 | _ = os.Setenv(KeyDBUser, dbUser)
34 | _ = os.Setenv(KeyDBPw, dbPw)
35 |
36 | _ = os.Setenv(KeyHostname, hostname)
37 | _ = os.Setenv(KeyLogLevel, logLvl)
38 | _ = os.Setenv(KeyHTTPS, fmt.Sprintf("%t", secure))
39 | _ = os.Setenv(KeyListen, listen)
40 | _ = os.Setenv(KeyStorage, pgSQL)
41 |
42 | var baseURL = fmt.Sprintf("https://%s", hostname)
43 | c, err := Load(env.TEST, time.Second)
44 | if err != nil {
45 | t.Errorf("Error loading env: %s", err)
46 | }
47 | // @todo(marius): we're no longer loading SQL db config env variables
48 | db := BackendConfig{}
49 | if db.Host != dbHost {
50 | t.Errorf("Invalid loaded value for %s: %s, expected %s", KeyDBHost, db.Host, dbHost)
51 | }
52 | if db.Port != dbPort {
53 | t.Errorf("Invalid loaded value for %s: %d, expected %d", KeyDBPort, db.Port, dbPort)
54 | }
55 | if db.Name != dbName {
56 | t.Errorf("Invalid loaded value for %s: %s, expected %s", KeyDBName, db.Name, dbName)
57 | }
58 | if db.User != dbUser {
59 | t.Errorf("Invalid loaded value for %s: %s, expected %s", KeyDBUser, db.User, dbUser)
60 | }
61 | if db.Pw != dbPw {
62 | t.Errorf("Invalid loaded value for %s: %s, expected %s", KeyDBPw, db.Pw, dbPw)
63 | }
64 |
65 | if c.Host != hostname {
66 | t.Errorf("Invalid loaded value for %s: %s, expected %s", KeyHostname, c.Host, hostname)
67 | }
68 | if c.Secure != secure {
69 | t.Errorf("Invalid loaded value for %s: %t, expected %t", KeyHTTPS, c.Secure, secure)
70 | }
71 | if c.Listen != listen {
72 | t.Errorf("Invalid loaded value for %s: %s, expected %s", KeyListen, c.Listen, listen)
73 | }
74 | if c.Storage != pgSQL {
75 | t.Errorf("Invalid loaded value for %s: %s, expected %s", KeyStorage, c.Storage, pgSQL)
76 | }
77 | if c.BaseURL != baseURL {
78 | t.Errorf("Invalid loaded BaseURL value: %s, expected %s", c.BaseURL, baseURL)
79 | }
80 | }
81 | {
82 | os.Setenv(KeyStorage, boltDB)
83 | c, err := Load(env.TEST, time.Second)
84 | if err != nil {
85 | t.Errorf("Error loading env: %s", err)
86 | }
87 | var tmp = strings.TrimRight(os.TempDir(), "/")
88 | if strings.TrimRight(c.StoragePath, "/") != tmp {
89 | t.Errorf("Invalid loaded boltdb dir value: %s, expected %s", c.StoragePath, tmp)
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/internal/config/storage_all.go:
--------------------------------------------------------------------------------
1 | //go:build storage_all || (!storage_fs && !storage_boltdb && !storage_badger && !storage_sqlite)
2 |
3 | package config
4 |
5 | const DefaultStorage = StorageFS
6 |
--------------------------------------------------------------------------------
/internal/config/storage_badger.go:
--------------------------------------------------------------------------------
1 | // +build storage_badger
2 |
3 | package config
4 |
5 | const DefaultStorage = StorageBadger
6 |
--------------------------------------------------------------------------------
/internal/config/storage_boltdb.go:
--------------------------------------------------------------------------------
1 | // +build storage_boltdb
2 |
3 | package config
4 |
5 | const DefaultStorage = StorageBoltDB
6 |
--------------------------------------------------------------------------------
/internal/config/storage_fs.go:
--------------------------------------------------------------------------------
1 | // +build storage_fs
2 |
3 | package config
4 |
5 | const DefaultStorage = StorageFS
6 |
--------------------------------------------------------------------------------
/internal/config/storage_sqlite.go:
--------------------------------------------------------------------------------
1 | // +build storage_sqlite
2 |
3 | package config
4 |
5 | const DefaultStorage = StorageSqlite
6 |
--------------------------------------------------------------------------------
/internal/env/env.go:
--------------------------------------------------------------------------------
1 | package env
2 |
3 | import "strings"
4 |
5 | // Type is a local type alias for the environment types
6 | type Type string
7 |
8 | // DEV environment
9 | const DEV Type = "dev"
10 |
11 | // PROD environment
12 | const PROD Type = "prod"
13 |
14 | // QA environment
15 | const QA Type = "qa"
16 |
17 | // TEST environment
18 | const TEST Type = "test"
19 |
20 | // Types represents the allowed types
21 | var Types = [...]Type{
22 | PROD,
23 | QA,
24 | DEV,
25 | TEST,
26 | }
27 |
28 | func ValidTypeOrDev(typ Type) Type {
29 | if ValidType(typ) {
30 | return Type(typ)
31 | }
32 |
33 | return DEV
34 | }
35 |
36 | func ValidType(typ Type) bool {
37 | for _, t := range Types {
38 | if strings.ToLower(string(typ)) == strings.ToLower(string(t)) {
39 | return true
40 | }
41 | }
42 | return false
43 | }
44 |
45 | func (e Type) IsProd() bool {
46 | return strings.Contains(string(e), string(PROD))
47 | }
48 | func (e Type) IsQA() bool {
49 | return strings.Contains(string(e), string(QA))
50 | }
51 | func (e Type) IsTest() bool {
52 | return strings.Contains(string(e), string(TEST))
53 | }
54 | func (e Type) IsDev() bool {
55 | return strings.Contains(string(e), string(DEV))
56 | }
57 |
--------------------------------------------------------------------------------
/internal/env/env_test.go:
--------------------------------------------------------------------------------
1 | package env
2 |
3 | import "testing"
4 |
5 | func TestType_IsProd(t *testing.T) {
6 | prod := PROD
7 | if !prod.IsProd() {
8 | t.Errorf("%T %s should have been production", prod, prod)
9 | }
10 | qa := QA
11 | if qa.IsProd() {
12 | t.Errorf("%T %s should not have been production", qa, qa)
13 | }
14 | dev := DEV
15 | if dev.IsProd() {
16 | t.Errorf("%T %s should not have been production", dev, dev)
17 | }
18 | test := TEST
19 | if test.IsProd() {
20 | t.Errorf("%T %s should not have been production", test, test)
21 | }
22 | rand := Type("Random")
23 | if rand.IsProd() {
24 | t.Errorf("%T %s should not have been production", rand, rand)
25 | }
26 | }
27 |
28 | func TestType_IsQA(t *testing.T) {
29 | qa := QA
30 | if !qa.IsQA() {
31 | t.Errorf("%T %s should not have been qa", qa, qa)
32 | }
33 | prod := PROD
34 | if prod.IsQA() {
35 | t.Errorf("%T %s should not have been qa", prod, prod)
36 | }
37 | dev := DEV
38 | if dev.IsQA() {
39 | t.Errorf("%T %s should not have been qa", dev, dev)
40 | }
41 | test := TEST
42 | if test.IsQA() {
43 | t.Errorf("%T %s shouldhave been qa", test, test)
44 | }
45 | rand := Type("Random")
46 | if rand.IsQA() {
47 | t.Errorf("%T %s should not have been qa", rand, rand)
48 | }
49 | }
50 |
51 | func TestType_IsTest(t *testing.T) {
52 | test := TEST
53 | if !test.IsTest() {
54 | t.Errorf("%T %s should have been test", test, test)
55 | }
56 | prod := PROD
57 | if prod.IsTest() {
58 | t.Errorf("%T %s should not have been test", prod, prod)
59 | }
60 | qa := QA
61 | if qa.IsTest() {
62 | t.Errorf("%T %s should not have been test", qa, qa)
63 | }
64 | dev := DEV
65 | if dev.IsTest() {
66 | t.Errorf("%T %s should not have been test", dev, dev)
67 | }
68 | rand := Type("Random")
69 | if rand.IsTest() {
70 | t.Errorf("%T %s should not have been test", rand, rand)
71 | }
72 | }
73 |
74 | func TestValidTypeOrDev(t *testing.T) {
75 | prod := PROD
76 | if prod != ValidTypeOrDev(prod) {
77 | t.Errorf("%T %s should have been valid, received %s", prod, prod, ValidTypeOrDev(prod))
78 | }
79 | qa := QA
80 | if qa != ValidTypeOrDev(qa) {
81 | t.Errorf("%T %s should have been valid, received %s", qa, qa, ValidTypeOrDev(qa))
82 | }
83 | test := TEST
84 | if test != ValidTypeOrDev(test) {
85 | t.Errorf("%T %s should have been valid, received %s", test, test, ValidTypeOrDev(test))
86 | }
87 | dev := DEV
88 | if dev != ValidTypeOrDev(dev) {
89 | t.Errorf("%T %s should have been valid, received %s", dev, dev, ValidTypeOrDev(dev))
90 | }
91 | rand := "Random"
92 | if dev != ValidTypeOrDev(Type(rand)) {
93 | t.Errorf("%T %s should not have been valid, received %s", rand, rand, ValidTypeOrDev(Type(rand)))
94 | }
95 | }
96 |
97 | func TestValidType(t *testing.T) {
98 | prod := PROD
99 | if !ValidType(prod) {
100 | t.Errorf("%T %s should have been valid", prod, prod)
101 | }
102 | qa := QA
103 | if !ValidType(qa) {
104 | t.Errorf("%T %s should have been valid", qa, qa)
105 | }
106 | dev := DEV
107 | if !ValidType(dev) {
108 | t.Errorf("%T %s should have been valid", dev, dev)
109 | }
110 | test := TEST
111 | if !ValidType(test) {
112 | t.Errorf("%T %s should have been valid", test, test)
113 | }
114 | rand := "Random"
115 | if ValidType(Type(rand)) {
116 | t.Errorf("%T %s should not have been valid", Type(rand), rand)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/internal/storage/all.go:
--------------------------------------------------------------------------------
1 | //go:build storage_all || (!storage_boltdb && !storage_fs && !storage_badger && !storage_sqlite)
2 |
3 | package storage
4 |
5 | import (
6 | "errors"
7 | "time"
8 |
9 | vocab "github.com/go-ap/activitypub"
10 | http "github.com/go-ap/errors"
11 | "github.com/go-ap/fedbox/internal/config"
12 | "github.com/go-ap/processing"
13 | "github.com/go-ap/storage-badger"
14 | "github.com/go-ap/storage-boltdb"
15 | "github.com/go-ap/storage-fs"
16 | "github.com/go-ap/storage-sqlite"
17 | )
18 |
19 | func BootstrapFn(opt config.Options) error {
20 | if opt.Storage == config.StorageBoltDB {
21 | c := boltdb.Config{Path: opt.BaseStoragePath()}
22 | return boltdb.Bootstrap(c)
23 | }
24 | if opt.Storage == config.StorageBadger {
25 | c := badger.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache}
26 | return badger.Bootstrap(c)
27 | }
28 | if opt.Storage == config.StorageFS {
29 | c := fs.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache, UseIndex: opt.UseIndex}
30 | return fs.Bootstrap(c)
31 | }
32 | if opt.Storage == config.StorageSqlite {
33 | c := sqlite.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache}
34 | return sqlite.Bootstrap(c)
35 | }
36 | return http.NotImplementedf("Invalid storage type %s", opt.Storage)
37 | }
38 |
39 | func CleanFn(opt config.Options) error {
40 | if opt.Storage == config.StorageBoltDB {
41 | c := boltdb.Config{Path: opt.BaseStoragePath()}
42 | return boltdb.Clean(c)
43 | }
44 | if opt.Storage == config.StorageBadger {
45 | c := badger.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache}
46 | return badger.Clean(c)
47 | }
48 | if opt.Storage == config.StorageFS {
49 | conf := fs.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache, UseIndex: opt.UseIndex}
50 | return fs.Clean(conf)
51 | }
52 | if opt.Storage == config.StorageSqlite {
53 | c := sqlite.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache}
54 | return sqlite.Clean(c)
55 | }
56 | return http.NotImplementedf("Invalid storage type %s", opt.Storage)
57 | }
58 |
59 | func CreateService(opt config.Options, self vocab.Item) (err error) {
60 | var r processing.WriteStore
61 | if opt.Storage == config.StorageBoltDB {
62 | c := boltdb.Config{Path: opt.BaseStoragePath()}
63 | r, err = boltdb.New(c)
64 | }
65 | if opt.Storage == config.StorageBadger {
66 | c := badger.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache}
67 | r, err = badger.New(c)
68 | }
69 | if opt.Storage == config.StorageFS {
70 | c := fs.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache, UseIndex: opt.UseIndex}
71 | r, err = fs.New(c)
72 | }
73 | if opt.Storage == config.StorageSqlite {
74 | c := sqlite.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache}
75 | r, err = sqlite.New(c)
76 | }
77 | if err != nil {
78 | return err
79 | }
80 | self, err = r.Save(self)
81 | if err != nil {
82 | return err
83 | }
84 | rr, ok := r.(processing.CollectionStore)
85 | if !ok {
86 | return nil
87 | }
88 | col := func(iri vocab.IRI) vocab.CollectionInterface {
89 | return &vocab.OrderedCollection{
90 | ID: iri,
91 | Type: vocab.OrderedCollectionType,
92 | Published: time.Now().UTC(),
93 | AttributedTo: self,
94 | CC: vocab.ItemCollection{vocab.PublicNS},
95 | }
96 | }
97 | return vocab.OnActor(self, func(service *vocab.Actor) error {
98 | var multi error
99 | for _, stream := range service.Streams {
100 | if _, err := rr.Create(col(stream.GetID())); err != nil {
101 | multi = errors.Join(multi, err)
102 | }
103 | }
104 | return multi
105 | })
106 | }
107 |
--------------------------------------------------------------------------------
/internal/storage/badger.go:
--------------------------------------------------------------------------------
1 | //go:build storage_badger
2 |
3 | package storage
4 |
5 | import (
6 | "github.com/go-ap/fedbox/internal/config"
7 | "github.com/go-ap/storage-badger"
8 | )
9 |
10 | func conf(opt config.Options) badger.Config {
11 | opt.Storage = config.DefaultStorage
12 | return badger.Config{Path: opt.BaseStoragePath(), CacheEnable: opt.StorageCache}
13 | }
14 |
15 | func BootstrapFn(opt config.Options) error {
16 | return badger.Bootstrap(conf(opt))
17 | }
18 |
19 | func CleanFn(opt config.Options) error {
20 | return badger.Clean(conf(opt))
21 | }
22 |
--------------------------------------------------------------------------------
/internal/storage/boltdb.go:
--------------------------------------------------------------------------------
1 | //go:build storage_boltdb
2 |
3 | package storage
4 |
5 | import (
6 | "github.com/go-ap/fedbox/internal/config"
7 | "github.com/go-ap/storage-boltdb"
8 | )
9 |
10 | func conf(opt config.Options) boltdb.Config {
11 | opt.Storage = config.DefaultStorage
12 | return boltdb.Config{Path: opt.BaseStoragePath()}
13 | }
14 |
15 | func BootstrapFn(opt config.Options) error {
16 | return boltdb.Bootstrap(conf(opt))
17 | }
18 |
19 | func CleanFn(opt config.Options) error {
20 | return boltdb.Clean(conf(opt))
21 | }
22 |
--------------------------------------------------------------------------------
/internal/storage/fs.go:
--------------------------------------------------------------------------------
1 | //go:build storage_fs
2 |
3 | package storage
4 |
5 | import (
6 | "github.com/go-ap/fedbox/internal/config"
7 | fs "github.com/go-ap/storage-fs"
8 | )
9 |
10 | func conf(opt config.Options) fs.Config {
11 | opt.Storage = config.DefaultStorage
12 | return fs.Config{
13 | Path: opt.BaseStoragePath(),
14 | CacheEnable: opt.StorageCache,
15 | UseIndex: opt.UseIndex,
16 | }
17 | }
18 |
19 | func BootstrapFn(opt config.Options) error {
20 | return fs.Bootstrap(conf(opt))
21 | }
22 |
23 | func CleanFn(opt config.Options) error {
24 | return fs.Clean(conf(opt))
25 | }
26 |
--------------------------------------------------------------------------------
/internal/storage/sqlite.go:
--------------------------------------------------------------------------------
1 | //go:build storage_sqlite
2 |
3 | package storage
4 |
5 | import (
6 | "github.com/go-ap/fedbox/internal/config"
7 | sqlite "github.com/go-ap/storage-sqlite"
8 | )
9 |
10 | func conf(opt config.Options) sqlite.Config {
11 | opt.Storage = config.DefaultStorage
12 | return sqlite.Config{
13 | Path: opt.BaseStoragePath(),
14 | CacheEnable: opt.StorageCache,
15 | }
16 | }
17 |
18 | func BootstrapFn(opt config.Options) error {
19 | return sqlite.Bootstrap(conf(opt))
20 | }
21 |
22 | func CleanFn(opt config.Options) error {
23 | return sqlite.Clean(conf(opt))
24 | }
25 |
--------------------------------------------------------------------------------
/internal/utils.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import vocab "github.com/go-ap/activitypub"
4 |
5 | func replaceHostInIRI(iri *vocab.IRI, repl vocab.IRI) *vocab.IRI {
6 | if iri.Contains(repl, false) {
7 | return iri
8 | }
9 |
10 | u, eu := iri.URL()
11 | if eu != nil {
12 | return iri
13 | }
14 |
15 | ru, er := repl.URL()
16 | if er != nil {
17 | return iri
18 | }
19 |
20 | u.Scheme = ru.Scheme
21 | u.Host = ru.Host
22 | *iri = vocab.IRI(u.String())
23 | return iri
24 | }
25 |
26 | func replaceHostInActor(a *vocab.Actor, repl vocab.IRI) {
27 | vocab.OnObject(a, func(o *vocab.Object) error {
28 | replaceHostInObject(o, repl)
29 | return nil
30 | })
31 | }
32 |
33 | func replaceHostInObject(o *vocab.Object, repl vocab.IRI) {
34 | replaceHostInIRI(&o.ID, repl)
35 | replaceHostInItem(o.AttributedTo, repl)
36 | replaceHostInItem(o.Attachment, repl)
37 | replaceHostInItem(o.Audience, repl)
38 | replaceHostInItem(o.Context, repl)
39 | replaceHostInItem(o.Generator, repl)
40 | replaceHostInItem(o.Icon, repl)
41 | replaceHostInItem(o.Image, repl)
42 | replaceHostInItem(o.InReplyTo, repl)
43 | replaceHostInItem(o.Location, repl)
44 | replaceHostInItem(o.Preview, repl)
45 | replaceHostInItem(o.Replies, repl)
46 | replaceHostInItem(o.Tag, repl)
47 | replaceHostInItem(o.To, repl)
48 | replaceHostInItem(o.Bto, repl)
49 | replaceHostInItem(o.CC, repl)
50 | replaceHostInItem(o.BCC, repl)
51 | replaceHostInItem(o.Likes, repl)
52 | replaceHostInItem(o.Shares, repl)
53 | }
54 |
55 | func replaceHostInActivity(o *vocab.Activity, repl vocab.IRI) {
56 | vocab.OnIntransitiveActivity(o, func(o *vocab.IntransitiveActivity) error {
57 | replaceHostInIntransitiveActivity(o, repl)
58 | return nil
59 | })
60 | replaceHostInItem(o.Object, repl)
61 | }
62 |
63 | func replaceHostInIntransitiveActivity(o *vocab.IntransitiveActivity, repl vocab.IRI) {
64 | vocab.OnObject(o, func(o *vocab.Object) error {
65 | replaceHostInObject(o, repl)
66 | return nil
67 | })
68 | replaceHostInItem(o.Actor, repl)
69 | replaceHostInItem(o.Target, repl)
70 | replaceHostInItem(o.Result, repl)
71 | replaceHostInItem(o.Origin, repl)
72 | replaceHostInItem(o.Instrument, repl)
73 | }
74 |
75 | func replaceHostInOrderedCollection(c *vocab.OrderedCollection, repl vocab.IRI) {}
76 | func replaceHostInOrderedCollectionPage(c *vocab.OrderedCollectionPage, repl vocab.IRI) {}
77 | func replaceHostInCollection(c *vocab.Collection, repl vocab.IRI) {}
78 | func replaceHostInCollectionPage(c *vocab.CollectionPage, repl vocab.IRI) {}
79 | func replaceHostInCollectionOfItems(c vocab.ItemCollection, repl vocab.IRI) {
80 | for _, it := range c {
81 | replaceHostInItem(it, repl)
82 | }
83 | }
84 |
85 | func replaceHostInItem(it vocab.Item, repl vocab.IRI) {
86 | if vocab.IsNil(it) {
87 | return
88 | }
89 | if it.IsCollection() {
90 | if it.GetType() == vocab.OrderedCollectionType {
91 | vocab.OnOrderedCollection(it, func(c *vocab.OrderedCollection) error {
92 | replaceHostInOrderedCollection(c, repl)
93 | return nil
94 | })
95 | }
96 | if it.GetType() == vocab.OrderedCollectionPageType {
97 | vocab.OnOrderedCollectionPage(it, func(c *vocab.OrderedCollectionPage) error {
98 | replaceHostInOrderedCollectionPage(c, repl)
99 | return nil
100 | })
101 | }
102 | if it.GetType() == vocab.CollectionType {
103 | vocab.OnCollection(it, func(c *vocab.Collection) error {
104 | replaceHostInCollection(c, repl)
105 | return nil
106 | })
107 | }
108 | if it.GetType() == vocab.CollectionPageType {
109 | vocab.OnCollectionPage(it, func(c *vocab.CollectionPage) error {
110 | replaceHostInCollectionPage(c, repl)
111 | return nil
112 | })
113 | }
114 | if it.GetType() == vocab.CollectionOfItems {
115 | vocab.OnItemCollection(it, func(c *vocab.ItemCollection) error {
116 | replaceHostInCollectionOfItems(*c, repl)
117 | return nil
118 | })
119 | }
120 | }
121 | if it.IsObject() {
122 | if vocab.IntransitiveActivityTypes.Contains(it.GetType()) {
123 | vocab.OnIntransitiveActivity(it, func(a *vocab.IntransitiveActivity) error {
124 | replaceHostInIntransitiveActivity(a, repl)
125 | return nil
126 | })
127 | }
128 | if vocab.ActivityTypes.Contains(it.GetType()) {
129 | vocab.OnActivity(it, func(a *vocab.Activity) error {
130 | replaceHostInActivity(a, repl)
131 | return nil
132 | })
133 | }
134 | if vocab.ActorTypes.Contains(it.GetType()) {
135 | vocab.OnActor(it, func(a *vocab.Actor) error {
136 | replaceHostInActor(a, repl)
137 | return nil
138 | })
139 | }
140 | if vocab.ObjectTypes.Contains(it.GetType()) {
141 | vocab.OnObject(it, func(o *vocab.Object) error {
142 | replaceHostInObject(o, repl)
143 | return nil
144 | })
145 | }
146 | }
147 | if it.IsLink() {
148 | l := it.GetLink()
149 | // FIXME(marius): because
150 | it = *replaceHostInIRI(&l, repl)
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/metatada.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/rand"
7 | "crypto/rsa"
8 | "crypto/x509"
9 | "encoding/pem"
10 | "fmt"
11 |
12 | vocab "github.com/go-ap/activitypub"
13 | "github.com/go-ap/auth"
14 | "github.com/go-ap/errors"
15 | "github.com/go-ap/fedbox/storage"
16 | "github.com/go-ap/processing"
17 | "golang.org/x/crypto/ed25519"
18 | )
19 |
20 | const (
21 | KeyTypeECDSA = "ECDSA"
22 | KeyTypeED25519 = "ED25519"
23 | KeyTypeRSA = "RSA"
24 | )
25 |
26 | func AddKeyToItem(metaSaver storage.MetadataTyper, it vocab.Item, typ string) error {
27 | if err := vocab.OnActor(it, AddKeyToPerson(metaSaver, typ)); err != nil {
28 | return errors.Annotatef(err, "failed to process actor: %s", it.GetID())
29 | }
30 | st, ok := metaSaver.(processing.Store)
31 | if !ok {
32 | return errors.Newf("invalid item store, failed to save actor: %s", it.GetID())
33 | }
34 | if _, err := st.Save(it); err != nil {
35 | return errors.Annotatef(err, "failed to save actor: %s", it.GetID())
36 | }
37 | return nil
38 | }
39 |
40 | func AddKeyToPerson(metaSaver storage.MetadataTyper, typ string) func(act *vocab.Actor) error {
41 | // TODO(marius): add a way to pass if we should overwrite the keys
42 | // for now we'll assume that if we're calling this, we want to do it
43 | overwriteKeys := true
44 | return func(act *vocab.Actor) error {
45 | if !vocab.ActorTypes.Contains(act.Type) {
46 | return nil
47 | }
48 |
49 | m, _ := metaSaver.LoadMetadata(act.ID)
50 | if m == nil {
51 | m = new(auth.Metadata)
52 | }
53 | var pubB, prvB pem.Block
54 | if m.PrivateKey == nil || overwriteKeys {
55 | if typ == KeyTypeED25519 {
56 | pubB, prvB = GenerateECKeyPair()
57 | } else {
58 | pubB, prvB = GenerateRSAKeyPair()
59 | }
60 | m.PrivateKey = pem.EncodeToMemory(&prvB)
61 | if err := metaSaver.SaveMetadata(*m, act.ID); err != nil {
62 | return errors.Annotatef(err, "failed saving metadata for actor: %s", act.ID)
63 | }
64 | } else {
65 | pubB = publicKeyFrom(m.PrivateKey)
66 | }
67 | if len(pubB.Bytes) > 0 {
68 | act.PublicKey = vocab.PublicKey{
69 | ID: vocab.IRI(fmt.Sprintf("%s#main", act.ID)),
70 | Owner: act.ID,
71 | PublicKeyPem: string(pem.EncodeToMemory(&pubB)),
72 | }
73 | }
74 | return nil
75 | }
76 | }
77 |
78 | func publicKeyFrom(prvBytes []byte) pem.Block {
79 | prv, _ := pem.Decode(prvBytes)
80 | var pubKey crypto.PublicKey
81 | if key, _ := x509.ParseECPrivateKey(prvBytes); key != nil {
82 | pubKey = key.PublicKey
83 | }
84 | if key, _ := x509.ParsePKCS8PrivateKey(prv.Bytes); pubKey == nil && key != nil {
85 | switch k := key.(type) {
86 | case *rsa.PrivateKey:
87 | pubKey = k.PublicKey
88 | case *ecdsa.PrivateKey:
89 | pubKey = k.PublicKey
90 | case ed25519.PrivateKey:
91 | pubKey = k.Public()
92 | }
93 | }
94 | pubEnc, err := x509.MarshalPKIXPublicKey(pubKey)
95 | if err != nil {
96 | return pem.Block{}
97 | }
98 | return pem.Block{Type: "PUBLIC KEY", Bytes: pubEnc}
99 | }
100 |
101 | func GenerateRSAKeyPair() (pem.Block, pem.Block) {
102 | keyPrv, _ := rsa.GenerateKey(rand.Reader, 2048)
103 |
104 | keyPub := keyPrv.PublicKey
105 | pubEnc, err := x509.MarshalPKIXPublicKey(&keyPub)
106 | if err != nil {
107 | panic(err)
108 | }
109 | prvEnc, err := x509.MarshalPKCS8PrivateKey(keyPrv)
110 | if err != nil {
111 | panic(err)
112 | }
113 | p := pem.Block{
114 | Type: "PUBLIC KEY",
115 | Bytes: pubEnc,
116 | }
117 | r := pem.Block{
118 | Type: "PRIVATE KEY",
119 | Bytes: prvEnc,
120 | }
121 | return p, r
122 | }
123 |
124 | func GenerateECKeyPair() (pem.Block, pem.Block) {
125 | // TODO(marius): make this actually produce proper keys
126 | keyPub, keyPrv, _ := ed25519.GenerateKey(rand.Reader)
127 |
128 | pubEnc, err := x509.MarshalPKIXPublicKey(keyPub)
129 | if err != nil {
130 | panic(err)
131 | }
132 | prvEnc, err := x509.MarshalPKCS8PrivateKey(keyPrv)
133 | if err != nil {
134 | panic(err)
135 | }
136 | p := pem.Block{
137 | Type: "PUBLIC KEY",
138 | Bytes: pubEnc,
139 | }
140 | r := pem.Block{
141 | Type: "PRIVATE KEY",
142 | Bytes: prvEnc,
143 | }
144 | return p, r
145 | }
146 |
--------------------------------------------------------------------------------
/middleware.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import (
4 | "net/http"
5 | "path"
6 |
7 | "github.com/go-ap/errors"
8 | "github.com/go-chi/chi/v5"
9 | )
10 |
11 | func CleanRequestPath(next http.Handler) http.Handler {
12 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13 | rctx := chi.RouteContext(r.Context())
14 |
15 | routePath := rctx.RoutePath
16 | if routePath == "" {
17 | if r.URL.RawPath != "" {
18 | routePath = r.URL.RawPath
19 | } else {
20 | routePath = r.URL.Path
21 | }
22 | }
23 | rctx.RoutePath = path.Clean(routePath)
24 |
25 | next.ServeHTTP(w, r)
26 | })
27 | }
28 |
29 | var (
30 | errShuttingDown = errors.ServiceUnavailablef("server is shutting down")
31 | errOutOfOrder = errors.ServiceUnavailablef("temporarily out of order")
32 | )
33 |
34 | func OutOfOrderMw(f *FedBOX) func(next http.Handler) http.Handler {
35 | return func(next http.Handler) http.Handler {
36 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37 | if f.conf.ShuttingDown {
38 | next = errors.HandleError(errShuttingDown)
39 | } else if f.conf.MaintenanceMode {
40 | next = errors.HandleError(errOutOfOrder)
41 | }
42 | next.ServeHTTP(w, r)
43 | })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/middlewares_test.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import "testing"
4 |
5 | func TestActorFromAuthHeader(t *testing.T) {
6 | t.Skipf("TODO")
7 | }
8 |
9 | func TestRepo(t *testing.T) {
10 | t.Skipf("TODO")
11 | }
12 |
13 | func TestValidator(t *testing.T) {
14 | t.Skipf("TODO")
15 | }
16 |
--------------------------------------------------------------------------------
/oauth.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
--------------------------------------------------------------------------------
/routes.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import (
4 | "net/http"
5 |
6 | "git.sr.ht/~mariusor/lw"
7 | "github.com/go-ap/errors"
8 | "github.com/go-chi/chi/v5"
9 | "github.com/go-chi/chi/v5/middleware"
10 | )
11 |
12 | func (f *FedBOX) Routes() func(chi.Router) {
13 | return func(r chi.Router) {
14 | r.Use(middleware.RequestID)
15 | r.Use(middleware.RealIP)
16 | r.Use(CleanRequestPath)
17 | r.Use(lw.Middlewares(f.logger)...)
18 | r.Use(OutOfOrderMw(f))
19 | r.Use(SetCORSHeaders)
20 |
21 | r.Method(http.MethodGet, "/", HandleItem(f))
22 | r.Method(http.MethodHead, "/", HandleItem(f))
23 | // TODO(marius): we can separate here the FedBOX specific collections from the ActivityPub spec ones
24 | // using some regular expressions
25 | // Eg: "/{collection:(inbox|outbox|followed)}"
26 | // Eg: "/{collection:(activities|objects|actors|moderators|ignored|blocked|flagged)}"
27 | r.Route("/{collection}", f.CollectionRoutes(true))
28 |
29 | if f.conf.Env.IsDev() {
30 | r.Mount("/debug", middleware.Profiler())
31 | }
32 |
33 | r.Handle("/favicon.ico", errors.NotFound)
34 | r.NotFound(errors.NotFound.ServeHTTP)
35 | r.MethodNotAllowed(errors.HandleError(errors.MethodNotAllowedf("method not allowed")).ServeHTTP)
36 | }
37 | }
38 |
39 | func (f *FedBOX) CollectionRoutes(descend bool) func(chi.Router) {
40 | return func(r chi.Router) {
41 | r.Group(func(r chi.Router) {
42 | r.Method(http.MethodGet, "/", HandleCollection(f))
43 | r.Method(http.MethodHead, "/", HandleCollection(f))
44 | r.Method(http.MethodPost, "/", HandleActivity(f))
45 |
46 | r.Route("/{id}", func(r chi.Router) {
47 | r.Method(http.MethodGet, "/", HandleItem(f))
48 | r.Method(http.MethodHead, "/", HandleItem(f))
49 | if descend {
50 | r.Route("/{collection}", f.CollectionRoutes(false))
51 | }
52 | })
53 | })
54 | }
55 | }
56 |
57 | func SetCORSHeaders(next http.Handler) http.Handler {
58 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
59 | if r.Method == http.MethodOptions {
60 | w.Header().Set("Access-Control-Allow-Origin", "*")
61 | w.WriteHeader(http.StatusOK)
62 | return
63 | }
64 | next.ServeHTTP(w, r)
65 | })
66 | }
67 |
--------------------------------------------------------------------------------
/routes_test.go:
--------------------------------------------------------------------------------
1 | package fedbox
2 |
3 | import "testing"
4 |
5 | func TestCollectionRoutes(t *testing.T) {
6 | t.Skipf("TODO")
7 | }
8 |
9 | func TestRoutes(t *testing.T) {
10 | t.Skipf("TODO")
11 | }
12 |
--------------------------------------------------------------------------------
/storage/storage.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | vocab "github.com/go-ap/activitypub"
5 | "github.com/go-ap/auth"
6 | "github.com/go-ap/processing"
7 | "github.com/openshift/osin"
8 | )
9 |
10 | type clientSaver interface {
11 | // UpdateClient updates the client (identified by it's id) and replaces the values with the values of client.
12 | UpdateClient(c osin.Client) error
13 | // CreateClient stores the client in the database and returns an error, if something went wrong.
14 | CreateClient(c osin.Client) error
15 | // RemoveClient removes a client (identified by id) from the database. Returns an error if something went wrong.
16 | RemoveClient(id string) error
17 | }
18 |
19 | type clientLister interface {
20 | // ListClients lists existing clients
21 | ListClients() ([]osin.Client, error)
22 | GetClient(id string) (osin.Client, error)
23 | }
24 |
25 | type FullStorage interface {
26 | Open() error
27 | clientSaver
28 | clientLister
29 | processing.Store
30 | processing.KeyLoader
31 | MetadataTyper
32 | PasswordChanger
33 | osin.Storage
34 | }
35 |
36 | type CanBootstrap interface {
37 | CreateService(vocab.Service) error
38 | }
39 |
40 | type PasswordChanger interface {
41 | PasswordSet(vocab.Item, []byte) error
42 | PasswordCheck(vocab.Item, []byte) error
43 | }
44 |
45 | type MetadataTyper interface {
46 | LoadMetadata(vocab.IRI) (*auth.Metadata, error)
47 | SaveMetadata(auth.Metadata, vocab.IRI) error
48 | }
49 |
50 | type MimeTypeSaver interface {
51 | SaveNaturalLanguageValues(vocab.NaturalLanguageValues) error
52 | SaveMimeTypeContent(vocab.MimeType, vocab.NaturalLanguageValues) error
53 | }
54 |
55 | type Resetter interface {
56 | Reset()
57 | }
58 |
59 | type IRIChecker interface {
60 | IsLocalIRI(i vocab.IRI) bool
61 | }
62 |
63 | func IsLocalIRI(s processing.Store) processing.IRIValidator {
64 | if c, ok := s.(IRIChecker); ok {
65 | return c.IsLocalIRI
66 | }
67 | return func(i vocab.IRI) bool {
68 | return false
69 | }
70 | }
71 |
72 | type OptionFn func(s processing.Store) error
73 |
--------------------------------------------------------------------------------
/storage_all.go:
--------------------------------------------------------------------------------
1 | //go:build storage_all || (!storage_boltdb && !storage_fs && !storage_badger && !storage_sqlite)
2 |
3 | package fedbox
4 |
5 | import (
6 | "git.sr.ht/~mariusor/lw"
7 | "github.com/go-ap/errors"
8 | "github.com/go-ap/fedbox/internal/config"
9 | st "github.com/go-ap/fedbox/storage"
10 | "github.com/go-ap/storage-badger"
11 | "github.com/go-ap/storage-boltdb"
12 | "github.com/go-ap/storage-fs"
13 | "github.com/go-ap/storage-sqlite"
14 | )
15 |
16 | func getBadgerStorage(c config.Options, l lw.Logger) (st.FullStorage, error) {
17 | path := c.BaseStoragePath()
18 | l = l.WithContext(lw.Ctx{"path": path})
19 | l.Debugf("Using badger storage")
20 | conf := badger.Config{
21 | Path: path,
22 | LogFn: l.Debugf,
23 | ErrFn: l.Warnf,
24 | }
25 | db, err := badger.New(conf)
26 | if err != nil {
27 | return db, err
28 | }
29 | return db, nil
30 | }
31 |
32 | func getBoltStorage(c config.Options, l lw.Logger) (st.FullStorage, error) {
33 | path := c.BaseStoragePath()
34 | l = l.WithContext(lw.Ctx{"path": path})
35 | l.Debugf("Using boltdb storage")
36 | db, err := boltdb.New(boltdb.Config{
37 | Path: path,
38 | LogFn: l.Debugf,
39 | ErrFn: l.Warnf,
40 | })
41 | if err != nil {
42 | return nil, err
43 | }
44 | return db, nil
45 | }
46 |
47 | func getFsStorage(c config.Options, l lw.Logger) (st.FullStorage, error) {
48 | p := c.BaseStoragePath()
49 | l = l.WithContext(lw.Ctx{"path": p})
50 | l.Debugf("Using fs storage")
51 | db, err := fs.New(fs.Config{
52 | Path: p,
53 | CacheEnable: c.StorageCache,
54 | Logger: l,
55 | UseIndex: c.UseIndex,
56 | })
57 | if err != nil {
58 | return nil, err
59 | }
60 | return db, nil
61 | }
62 |
63 | func getSqliteStorage(c config.Options, l lw.Logger) (st.FullStorage, error) {
64 | path := c.BaseStoragePath()
65 | l = l.WithContext(lw.Ctx{"path": path})
66 | l.Debugf("Using sqlite storage")
67 | db, err := sqlite.New(sqlite.Config{
68 | Path: path,
69 | CacheEnable: c.StorageCache,
70 | LogFn: l.Debugf,
71 | ErrFn: l.Warnf,
72 | })
73 |
74 | if err != nil {
75 | return nil, errors.Annotatef(err, "unable to connect to sqlite storage")
76 | }
77 | return db, nil
78 | }
79 |
80 | func Storage(c config.Options, l lw.Logger) (st.FullStorage, error) {
81 | switch c.Storage {
82 | case config.StorageBoltDB:
83 | return getBoltStorage(c, l)
84 | case config.StorageBadger:
85 | return getBadgerStorage(c, l)
86 | case config.StorageSqlite:
87 | return getSqliteStorage(c, l)
88 | case config.StorageFS:
89 | return getFsStorage(c, l)
90 | }
91 | return nil, errors.NotImplementedf("Invalid storage type %s", c.Storage)
92 | }
93 |
--------------------------------------------------------------------------------
/storage_badger.go:
--------------------------------------------------------------------------------
1 | //go:build storage_badger
2 |
3 | package fedbox
4 |
5 | import (
6 | "git.sr.ht/~mariusor/lw"
7 | "github.com/go-ap/fedbox/internal/config"
8 | st "github.com/go-ap/fedbox/storage"
9 | "github.com/go-ap/storage-badger"
10 | )
11 |
12 | func Storage(c config.Options, l lw.Logger) (st.FullStorage, error) {
13 | c.Storage = config.DefaultStorage
14 | path := c.BaseStoragePath()
15 | l = l.WithContext(lw.Ctx{"path": path})
16 | l.Debugf("Using badger storage")
17 | conf := badger.Config{
18 | Path: path,
19 | LogFn: l.Debugf,
20 | ErrFn: l.Warnf,
21 | }
22 | db, err := badger.New(conf)
23 | if err != nil {
24 | return db, err
25 | }
26 | return db, nil
27 | }
28 |
--------------------------------------------------------------------------------
/storage_boltdb.go:
--------------------------------------------------------------------------------
1 | //go:build storage_boltdb
2 |
3 | package fedbox
4 |
5 | import (
6 | "git.sr.ht/~mariusor/lw"
7 | "github.com/go-ap/fedbox/internal/config"
8 | st "github.com/go-ap/fedbox/storage"
9 | "github.com/go-ap/storage-boltdb"
10 | )
11 |
12 | func Storage(c config.Options, l lw.Logger) (st.FullStorage, error) {
13 | c.Storage = config.DefaultStorage
14 | path := c.BaseStoragePath()
15 | l = l.WithContext(lw.Ctx{"path": path})
16 | l.Debugf("Using boltdb storage")
17 | db, err := boltdb.New(boltdb.Config{
18 | Path: path,
19 | LogFn: l.Debugf,
20 | ErrFn: l.Warnf,
21 | })
22 | if err != nil {
23 | return nil, err
24 | }
25 | return db, nil
26 | }
27 |
--------------------------------------------------------------------------------
/storage_fs.go:
--------------------------------------------------------------------------------
1 | //go:build storage_fs
2 |
3 | package fedbox
4 |
5 | import (
6 | "git.sr.ht/~mariusor/lw"
7 | "github.com/go-ap/fedbox/internal/config"
8 | st "github.com/go-ap/fedbox/storage"
9 | fs "github.com/go-ap/storage-fs"
10 | )
11 |
12 | func Storage(c config.Options, l lw.Logger) (st.FullStorage, error) {
13 | c.Storage = config.DefaultStorage
14 | p := c.BaseStoragePath()
15 | l = l.WithContext(lw.Ctx{"path": p})
16 | l.Debugf("Using fs storage")
17 | db, err := fs.New(fs.Config{
18 | Path: p,
19 | CacheEnable: c.StorageCache,
20 | Logger: l,
21 | UseIndex: c.UseIndex,
22 | })
23 | if err != nil {
24 | return nil, err
25 | }
26 | return db, nil
27 | }
28 |
--------------------------------------------------------------------------------
/storage_sqlite.go:
--------------------------------------------------------------------------------
1 | //go:build storage_sqlite
2 |
3 | package fedbox
4 |
5 | import (
6 | "git.sr.ht/~mariusor/lw"
7 | "github.com/go-ap/errors"
8 | "github.com/go-ap/fedbox/internal/config"
9 | st "github.com/go-ap/fedbox/storage"
10 | sqlite "github.com/go-ap/storage-sqlite"
11 | )
12 |
13 | func Storage(c config.Options, l lw.Logger) (st.FullStorage, error) {
14 | c.Storage = config.DefaultStorage
15 | path := c.BaseStoragePath()
16 | l = l.WithContext(lw.Ctx{"path": path})
17 | l.Debugf("Using sqlite storage")
18 | db, err := sqlite.New(sqlite.Config{
19 | Path: path,
20 | CacheEnable: c.StorageCache,
21 | LogFn: l.Debugf,
22 | ErrFn: l.Warnf,
23 | })
24 |
25 | if err != nil {
26 | return nil, errors.Annotatef(err, "unable to connect to sqlite storage")
27 | }
28 | return db, nil
29 | }
30 |
--------------------------------------------------------------------------------
/systemd/fedbox.service.in:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=FedBOX
3 |
4 | [Service]
5 | Type=simple
6 | User=fedbox
7 | WorkingDirectory=WORKING_DIR
8 | ExecReload=/bin/kill -SIGHUP $MAINPID
9 | Restart=on-failure
10 | ExecStart=/bin/fedbox
11 |
12 | [Install]
13 | WantedBy=default.target
14 |
--------------------------------------------------------------------------------
/systemd/fedbox.socket.in:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=FedBOX
3 |
4 | [Socket]
5 | ListenStream=LISTEN_HOST:LISTEN_PORT
6 |
7 | [Install]
8 | WantedBy=sockets.target
9 |
10 |
--------------------------------------------------------------------------------
/tests/.cache/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-ap/fedbox/187ccc1640ef0476ba532cf16ad34de27477d2a9/tests/.cache/.gitkeep
--------------------------------------------------------------------------------
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/test*
2 |
--------------------------------------------------------------------------------
/tests/Makefile:
--------------------------------------------------------------------------------
1 | GO := go
2 | TEST_FLAGS ?= -count=1
3 | STORAGE ?= all
4 |
5 | ENV ?= test
6 | BUILDFLAGS ?= -a
7 | TAGS = $(ENV) storage_$(STORAGE) integration
8 | TEST_TARGET = ./
9 |
10 | TEST := $(GO) test $(BUILDFLAGS)
11 |
12 | .PHONY: test integration clean
13 |
14 | .cache:
15 | mkdir -p .cache
16 |
17 | clean:
18 | @-$(RM) -rf ./.cache/$(ENV)
19 | @-$(RM) -rf ./.cache/*.bdb
20 |
21 | c2s: clean .cache
22 | $(TEST) $(TEST_FLAGS) -tags "$(TAGS) c2s" $(TEST_TARGET)
23 |
24 | s2s: clean .cache
25 | $(TEST) $(TEST_FLAGS) -tags "$(TAGS) s2s" $(TEST_TARGET)
26 |
27 | test: c2s s2s
28 |
29 | integration: test
30 |
--------------------------------------------------------------------------------
/tests/main_test.go:
--------------------------------------------------------------------------------
1 | //go:build integration
2 |
3 | package tests
4 |
5 | import (
6 | "flag"
7 | "os"
8 | "testing"
9 | )
10 |
11 | func TestMain(m *testing.M) {
12 | flag.BoolVar(&Verbose, "verbose", false, "enable more verbose logging")
13 | flag.Parse()
14 |
15 | if st := m.Run(); st != 0 {
16 | os.Exit(st)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/mockapp.go:
--------------------------------------------------------------------------------
1 | //go:build integration
2 |
3 | package tests
4 |
5 | import (
6 | "bytes"
7 | "crypto"
8 | "crypto/ecdsa"
9 | "crypto/rsa"
10 | "crypto/x509"
11 | "embed"
12 | "encoding/pem"
13 | "fmt"
14 | "io/fs"
15 | "os"
16 | "path"
17 | "path/filepath"
18 | "strings"
19 | "testing"
20 | "text/template"
21 |
22 | "git.sr.ht/~mariusor/lw"
23 | vocab "github.com/go-ap/activitypub"
24 | "github.com/go-ap/auth"
25 | "github.com/go-ap/fedbox"
26 | "github.com/go-ap/fedbox/internal/cmd"
27 | "github.com/go-ap/fedbox/internal/config"
28 | ls "github.com/go-ap/fedbox/storage"
29 | "github.com/go-ap/jsonld"
30 | "golang.org/x/crypto/ed25519"
31 | )
32 |
33 | func jsonldMarshal(i vocab.Item) string {
34 | j, err := jsonld.Marshal(i)
35 | if err != nil {
36 | panic(fmt.Sprintf("%+v", err))
37 | }
38 | return string(j)
39 | }
40 |
41 | //go:embed mocks
42 | var mocks embed.FS
43 |
44 | func loadMockJson(file string, model any) func() (string, error) {
45 | data, err := fs.ReadFile(mocks, file)
46 | if err != nil {
47 | return func() (string, error) { return "", err }
48 | }
49 | data = bytes.Trim(data, "\x00")
50 |
51 | t := template.Must(template.New(fmt.Sprintf("mock_%s", path.Base(file))).
52 | Funcs(template.FuncMap{"json": jsonldMarshal}).Parse(string(data)))
53 |
54 | return func() (string, error) {
55 | raw := bytes.Buffer{}
56 | err = t.Execute(&raw, model)
57 | return raw.String(), err
58 | }
59 | }
60 |
61 | func addMockObjects(r ls.FullStorage, obj vocab.ItemCollection) error {
62 | var err error
63 | for _, it := range obj {
64 | if it.GetLink() == "" {
65 | continue
66 | }
67 | if it.GetLink().Equals(vocab.IRI(service.ID), false) {
68 | self, _ := vocab.ToActor(it)
69 | if err = fedbox.AddKeyToPerson(r, fedbox.KeyTypeRSA)(self); err != nil {
70 | return err
71 | }
72 | if self.ID.Equals(vocab.IRI(service.ID), false) {
73 | service.PublicKey = self.PublicKey
74 | service.PrivateKey, _ = r.LoadKey(vocab.IRI(service.ID))
75 | }
76 | }
77 | if it, err = r.Save(it); err != nil {
78 | return err
79 | }
80 | }
81 | return nil
82 | }
83 |
84 | func cleanDB(t *testing.T, opt config.Options) {
85 | if opt.Storage == "all" {
86 | opt.Storage = config.StorageFS
87 | }
88 | t.Logf("resetting %q db: %s", opt.Storage, opt.StoragePath)
89 | if err := cmd.Reset(opt); err != nil {
90 | t.Error(err)
91 | }
92 | if t.Failed() {
93 | return
94 | }
95 |
96 | tempPath, err := os.Getwd()
97 | if err != nil {
98 | t.Logf("Unable to get current path: %s", err)
99 | }
100 |
101 | tempPath = path.Clean(filepath.Join(tempPath, filepath.Dir(opt.StoragePath)))
102 | if !strings.Contains(tempPath, ".cache") {
103 | t.Logf("Unable to clean path: %s", tempPath)
104 | return
105 | }
106 | t.Logf("Removing path: %s", tempPath)
107 |
108 | //As we're using t.TempDir for the storage path, we can remove it fully
109 | t.Logf("Removing path: %s", tempPath)
110 | if err = os.RemoveAll(tempPath); err != nil {
111 | t.Logf("Unable to remove path: %s: %s", tempPath, err)
112 | }
113 | }
114 |
115 | func publicKeyFrom(key crypto.PrivateKey) crypto.PublicKey {
116 | switch k := key.(type) {
117 | case *rsa.PrivateKey:
118 | return k.PublicKey
119 | case *ecdsa.PrivateKey:
120 | return k.PublicKey
121 | case ed25519.PrivateKey:
122 | return k.Public()
123 | }
124 | panic(fmt.Sprintf("Unknown private key type[%T] %v", key, key))
125 | return nil
126 | }
127 |
128 | func loadPrivateKeyFromDisk(file string) crypto.PrivateKey {
129 | data, err := fs.ReadFile(mocks, file)
130 | if err != nil {
131 | panic(fmt.Sprintf("%+v", err))
132 | }
133 | b, _ := pem.Decode(data)
134 | if b == nil {
135 | panic("failed decoding pem")
136 | }
137 | prvKey, err := x509.ParsePKCS8PrivateKey(b.Bytes)
138 | if err != nil {
139 | panic(fmt.Sprintf("%+v", err))
140 | }
141 | return prvKey
142 | }
143 |
144 | func loadMockFromDisk(file string, model any) vocab.Item {
145 | json, err := loadMockJson(file, model)()
146 | if err != nil {
147 | w, _ := os.Getwd()
148 | panic(fmt.Sprintf(" in path %s: %+v", w, err))
149 | }
150 | it, err := vocab.UnmarshalJSON([]byte(json))
151 | if err != nil {
152 | panic(fmt.Sprintf("%+v", err))
153 | }
154 | return it
155 | }
156 |
157 | func saveMocks(testData []string, config config.Options, db ls.FullStorage, l lw.Logger) error {
158 | if len(testData) == 0 {
159 | return nil
160 | }
161 |
162 | baseIRI := vocab.IRI(config.BaseURL)
163 | m := make(vocab.ItemCollection, 0)
164 | for _, mock := range testData {
165 | it := loadMockFromDisk(mock, nil)
166 | if !it.GetLink().Contains(baseIRI, false) {
167 | continue
168 | }
169 | if !m.Contains(it) {
170 | m = append(m, it)
171 | }
172 | }
173 | if err := addMockObjects(db, m); err != nil {
174 | return err
175 | }
176 |
177 | o, _ := cmd.New(db, config, l)
178 | if strings.Contains(defaultTestAccountC2S.ID, config.BaseURL) {
179 | if err := saveMetadataForActor(defaultTestAccountC2S, db.(ls.MetadataTyper)); err != nil {
180 | return err
181 | }
182 |
183 | if tok, err := o.GenAuthToken(defaultTestApp.ID, defaultTestAccountC2S.ID, nil); err == nil {
184 | defaultTestAccountC2S.AuthToken = tok
185 | }
186 | }
187 | if strings.Contains(defaultTestAccountS2S.ID, config.BaseURL) {
188 | if err := saveMetadataForActor(defaultTestAccountS2S, db.(ls.MetadataTyper)); err != nil {
189 | return err
190 | }
191 |
192 | if tok, err := o.GenAuthToken(defaultTestApp.ID, defaultTestAccountS2S.ID, nil); err == nil {
193 | defaultTestAccountS2S.AuthToken = tok
194 | }
195 | }
196 | return nil
197 | }
198 |
199 | func saveMetadataForActor(act testAccount, metaSaver ls.MetadataTyper) error {
200 | prvEnc, err := x509.MarshalPKCS8PrivateKey(act.PrivateKey)
201 | if err != nil {
202 | return err
203 | }
204 | r := pem.Block{Type: "PRIVATE KEY", Bytes: prvEnc}
205 | return metaSaver.SaveMetadata(
206 | auth.Metadata{PrivateKey: pem.EncodeToMemory(&r)},
207 | vocab.IRI(act.ID),
208 | )
209 | }
210 |
211 | func seedTestData(app *fedbox.FedBOX) error {
212 | db := app.Storage()
213 |
214 | act := loadMockFromDisk("mocks/c2s/actors/application.json", nil)
215 | if err := addMockObjects(db, vocab.ItemCollection{act}); err != nil {
216 | return err
217 | }
218 |
219 | return db.CreateClient(mockClient)
220 | }
221 |
222 | func getTestFedBOX(options config.Options) (*fedbox.FedBOX, error) {
223 | if options.Storage == "all" {
224 | options.Storage = config.StorageFS
225 | }
226 | options.AppName = "fedbox/integration-tests"
227 | options.Version = "HEAD"
228 | options.MastodonCompatible = true
229 |
230 | fields := lw.Ctx{"action": "running", "storage": options.Storage, "path": options.BaseStoragePath()}
231 |
232 | l := lw.Prod(lw.SetLevel(options.LogLevel), lw.SetOutput(os.Stdout))
233 | db, err := fedbox.Storage(options, l.WithContext(fields))
234 | if err != nil {
235 | return nil, err
236 | }
237 |
238 | a, err := fedbox.New(l, options, db)
239 | if err != nil {
240 | return nil, err
241 | }
242 |
243 | if err = seedTestData(a); err != nil {
244 | return nil, err
245 | }
246 |
247 | return a, nil
248 | }
249 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/activity-private.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "{{ .Type }}",
3 | "actor": "{{ .ActorID }}",
4 | "object": "{{ .ObjectID }}"
5 | }
6 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/activity.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "{{ .Type }}",
3 | "actor": "{{ .ActorID }}",
4 | "to": ["https://www.w3.org/ns/activitystreams#Public", "http://127.0.0.1:9998"],
5 | "object": "{{ .ObjectID }}"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/block-actor.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Block",
3 | "actor": "{{ .ID }}",
4 | "object": "http://127.0.0.1:9998/actors/58e877c7-067f-4842-960b-3896d76aa4ed"
5 | }
6 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/create-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/activities/1",
3 | "type": "Create",
4 | "actor": "http://127.0.0.1:9998/actors/2",
5 | "to": ["https://www.w3.org/ns/activitystreams#Public", "http://127.0.0.1:9998"],
6 | "object": "http://127.0.0.1:9998/objects/1"
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/create-2.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/activities/2",
3 | "type": "Create",
4 | "actor": "http://127.0.0.1:9998/actors/2",
5 | "to": ["https://www.w3.org/ns/activitystreams#Public", "http://127.0.0.1:9998"],
6 | "object": "http://127.0.0.1:9998/objects/2"
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/create-actor.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Create",
3 | "actor": "{{ .ID }}",
4 | "object": {
5 | "type": "Person",
6 | "published": "2019-05-10T22:16:40Z",
7 | "to": [
8 | "https://www.w3.org/ns/activitystreams#Public"
9 | ],
10 | "name": "Jane Doe",
11 | "preferredUsername": "jennyjane"
12 | },
13 | "to": [
14 | "https://www.w3.org/ns/activitystreams#Public"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/create-article.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Create",
3 | "actor": "{{ .ID }}",
4 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
5 | "object": {
6 | "type": "Article",
7 | "published": "2018-08-31T17:17:11Z",
8 | "attributedTo": "{{ .ID }}",
9 | "content": "Hello world
",
10 | "to": ["https://www.w3.org/ns/activitystreams#Public", "http://127.0.0.1:9998"]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/create-object-with-federated-cc.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Create",
3 | "actor": "{{ .ActorID }}",
4 | "object": {{ .Object | json }},
5 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ],
6 | "cc": [ "http://127.0.2.1:9999/actors/666" ]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/create-object.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Create",
3 | "actor": "{{ .ActorID }}",
4 | "object": {{ .Object | json }},
5 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ]
6 | }
7 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/question-with-anyOf.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Question",
3 | "to": ["https://www.w3.org/ns/activitystreams#Public", "http://127.0.0.1:9998"],
4 | "actor": "{{ .ActorID }}",
5 | "name": "Some question",
6 | "anyOf": [
7 | {
8 | "name": "Answer 1"
9 | },
10 | {
11 | "name": "Answer 2"
12 | },
13 | {
14 | "name": "Answer 3"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/question-with-oneOf.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Question",
3 | "to": ["https://www.w3.org/ns/activitystreams#Public", "http://127.0.0.1:9998"],
4 | "actor": "{{ .ActorID }}",
5 | "name": "Some question",
6 | "oneOf": [
7 | {
8 | "name": "Answer 1"
9 | },
10 | {
11 | "name": "Answer 2"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/question.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Question",
3 | "to": ["https://www.w3.org/ns/activitystreams#Public", "http://127.0.0.1:9998"],
4 | "actor": "{{ .ActorID }}"
5 | }
6 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/activities/update-actor.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Update",
3 | "actor": "{{ .ID }}",
4 | "object": {
5 | "id": "{{ .ID }}",
6 | "type": "Person",
7 | "to": [
8 | "{{ .ID }}/inbox",
9 | "https://www.w3.org/ns/activitystreams#Public"
10 | ],
11 | "name": "Jane Doe",
12 | "summary": "I am a test user, and I am fine with that.",
13 | "preferredUsername": "jennyjane"
14 | },
15 | "to": [
16 | "https://www.w3.org/ns/activitystreams#Public"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-admin.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/1",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "url": "http://127.0.0.1:9998/actors/1",
9 | "inbox": "http://127.0.0.1:9998/actors/1/inbox",
10 | "outbox": "http://127.0.0.1:9998/actors/1/outbox",
11 | "preferredUsername": "admin",
12 | "endpoints": {
13 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
14 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
15 | "sharedInbox": "http://127.0.0.1:9998/inbox"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_a.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/2",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/2/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/2/outbox",
10 | "preferredUsername": "element_a",
11 | "tag": ["http://127.0.0.1:9998/objects/t1"],
12 | "endpoints": {
13 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
14 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
15 | "sharedInbox": "http://127.0.0.1:9998/inbox"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_b.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/3",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/3/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/3/outbox",
10 | "preferredUsername": "element_b",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_c.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/4",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/4/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/4/outbox",
10 | "preferredUsername": "element_c",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_d.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/5",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/5/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/5/outbox",
10 | "preferredUsername": "element_d",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_e.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/6",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/6/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/6/outbox",
10 | "preferredUsername": "element_e",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_f.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/7",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/7/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/7/outbox",
10 | "preferredUsername": "element_f",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_g.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/8",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/8/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/8/outbox",
10 | "preferredUsername": "element_g",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_h.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/9",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/9/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/9/outbox",
10 | "preferredUsername": "element_h",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-element_i.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/10",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.0.1:9998",
8 | "inbox": "http://127.0.0.1:9998/actors/10/inbox",
9 | "outbox": "http://127.0.0.1:9998/actors/10/outbox",
10 | "preferredUsername": "element_i",
11 | "endpoints": {
12 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
13 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
14 | "sharedInbox": "http://127.0.0.1:9998/inbox"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-extra.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/58e877c7-067f-4842-960b-3896d76aa4ed",
3 | "type": "Person",
4 | "generator": "http://127.0.0.1:9998",
5 | "url": "http://127.0.0.1:9998/~extra",
6 | "to": [
7 | "https://www.w3.org/ns/activitystreams#Public"
8 | ],
9 | "published": "2019-11-23T19:12:05.644483296Z",
10 | "updated": "2019-11-23T19:12:05.644483296Z",
11 | "inbox": "http://127.0.0.1:9998/actors/58e877c7-067f-4842-960b-3896d76aa4ed/inbox",
12 | "outbox": "http://127.0.0.1:9998/actors/58e877c7-067f-4842-960b-3896d76aa4ed/outbox",
13 | "preferredUsername": "extra"
14 | }
15 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/actor-johndoe.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.0.1:9998",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "content": "Generated actor",
8 | "generator": "http://127.0.0.1:9998",
9 | "published": "2019-08-11T13:14:47.726030449+02:00",
10 | "summary": "Generated actor",
11 | "updated": "2019-08-11T13:14:47.726030449+02:00",
12 | "url": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1",
13 | "inbox": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1/inbox",
14 | "outbox": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1/outbox",
15 | "liked": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1/liked",
16 | "preferredUsername": "johndoe",
17 | "name": "Johnathan Doe",
18 | "endpoints": {
19 | "oauthAuthorizationEndpoint": "http://127.0.0.1:9998/oauth/authorize",
20 | "oauthTokenEndpoint": "http://127.0.0.1:9998/oauth/token",
21 | "sharedInbox": "http://127.0.0.1:9998/inbox"
22 | },
23 | "publicKey": {
24 | "id": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1#main-key",
25 | "owner": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1",
26 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApkL3B0AX7Xr7p0+tdWskfKh48pk2vE5eAbhZ+jznwoEFr4RaXwPhbbAsUoLacEf4iwcfODTCmKpwoedTmd01IWgITtf5oLfzAHkhnV0QbCeJT/craH2drDeQzJGecgdu8o4JbxYwSZ33Lff0QS1qVAIKiMmeRKJz/i8qyMky9uyWvFRRMAQZWDv8EAPLyAT99PpxXYZLr7uTXwerOvnkJeLLj0XNjYx9nAQOig3zP9D5sghYVSjHkWuS87Sbc1OzYcE1PY+OHnuYRPpR1WXH880WiEaK4v1kFoCl62UCR96hmQFHvAc0eTC2EDV4nnM5uLbF8/p0gMy9mPYfTnfJEwIDAQAB\n-----END PUBLIC KEY-----"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/application-11.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/11",
3 | "type": "Application",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "url": "http://127.0.0.1:9998/callback",
8 | "inbox": "http://127.0.0.1:9998/actors/11/inbox"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/application-12.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/12",
3 | "type": "Application",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "url": "http://127.0.0.1:9998/callback",
8 | "inbox": "http://127.0.0.1:9998/actors/12/inbox"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/application-13.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/13",
3 | "type": "Application",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "url": "http://127.0.0.1:9998/callback",
8 | "inbox": "http://127.0.0.1:9998/actors/13/inbox"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/application-14.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/14",
3 | "type": "Application",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "url": "http://127.0.0.1:9998/callback",
8 | "inbox": "http://127.0.0.1:9998/actors/14/inbox"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/application-15.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/15",
3 | "type": "Application",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "url": "http://127.0.0.1:9998/callback",
8 | "inbox": "http://127.0.0.1:9998/actors/15/inbox"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/application.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/23767f95-8ea0-40ba-a6ef-b67284e1cdb1",
3 | "type": "Application",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": [
6 | "https://www.w3.org/ns/activitystreams#Public"
7 | ],
8 | "generator": "http://127.0.0.1:9998",
9 | "published": "2019-08-24T19:15:38.684863306+02:00",
10 | "summary": "Generated actor",
11 | "updated": "2019-08-24T19:15:38.684863306+02:00",
12 | "url": "http://127.0.0.1:9998/callback",
13 | "inbox": "http://127.0.0.1:9998/actors/23767f95-8ea0-40ba-a6ef-b67284e1cdb1/inbox",
14 | "preferredUsername": "oauth-client-app-23767f95-8ea0-40ba-a6ef-b67284e1cdb1"
15 | }
16 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/group-16.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/16",
3 | "type": "Group",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "published": "2019-08-24T19:15:38.684863306+02:00",
8 | "updated": "2019-08-24T19:15:38.684863306+02:00",
9 | "url": "http://127.0.0.1:9998/callback",
10 | "inbox": "http://127.0.0.1:9998/actors/16/inbox",
11 | "followers": "http://127.0.0.1:9998/actors/16/followers",
12 | "name": "Just a group"
13 | }
14 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/group-17.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/17",
3 | "type": "Group",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "published": "2019-08-24T19:15:38.684863306+02:00",
8 | "updated": "2019-08-24T19:15:38.684863306+02:00",
9 | "url": "http://127.0.0.1:9998/callback",
10 | "inbox": "http://127.0.0.1:9998/actors/17/inbox",
11 | "followers": "http://127.0.0.1:9998/actors/17/followers",
12 | "name": "Another group"
13 | }
14 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/group-18.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/18",
3 | "type": "Group",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "published": "2019-08-24T19:15:38.684863306+02:00",
8 | "updated": "2019-08-24T19:15:38.684863306+02:00",
9 | "url": "http://127.0.0.1:9998/callback",
10 | "inbox": "http://127.0.0.1:9998/actors/18/inbox",
11 | "followers": "http://127.0.0.1:9998/actors/18/followers",
12 | "name": "Group 18"
13 | }
14 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/group-19.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/19",
3 | "type": "Group",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "published": "2019-08-24T19:15:38.684863306+02:00",
8 | "updated": "2019-08-24T19:15:38.684863306+02:00",
9 | "url": "http://127.0.0.1:9998/callback",
10 | "inbox": "http://127.0.0.1:9998/actors/19/inbox",
11 | "followers": "http://127.0.0.1:9998/actors/19/followers",
12 | "name": "Grouppe"
13 | }
14 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/group-20.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/actors/20",
3 | "type": "Group",
4 | "attributedTo": "http://127.0.0.1:9998",
5 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
6 | "to": ["http://127.0.0.1:9998", "https://www.w3.org/ns/activitystreams#Public"],
7 | "published": "2019-08-24T19:15:38.684863306+02:00",
8 | "updated": "2019-08-24T19:15:38.684863306+02:00",
9 | "url": "http://127.0.0.1:9998/callback",
10 | "inbox": "http://127.0.0.1:9998/actors/20/inbox",
11 | "followers": "http://127.0.0.1:9998/actors/20/followers",
12 | "name": "Admins"
13 | }
14 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/actors/service.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/",
4 | "type": "Service",
5 | "name": "self",
6 | "attributedTo": "https://github.com/mariusor",
7 | "audience": [
8 | "https://www.w3.org/ns/activitystreams#Public"
9 | ],
10 | "summary": "Generic ActivityPub service",
11 | "inbox": "http://127.0.0.1:9998/inbox",
12 | "outbox": "http://127.0.0.1:9998/outbox"
13 | }
14 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/note-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/objects/1",
4 | "type": "Note",
5 | "inReplyTo": "http://127.0.0.1:9998/objects/2",
6 | "attributedTo": "http://127.0.0.1:9998/actors/2",
7 | "content": "Hello
\nworld
!
\n",
8 | "mediaType": "text/html",
9 | "url": "https://example.com/1",
10 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
11 | "replies": "http://127.0.0.1:9998/objects/1/replies",
12 | "likes": "http://127.0.0.1:9998/objects/1/likes",
13 | "shares": "http://127.0.0.1:9998/objects/1/shares",
14 | "source": {
15 | "content": "Hello\n\n`world`!",
16 | "mediaType": "text/markdown"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/note-replyTo-1-2-5.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/objects/6",
4 | "type": "Note",
5 | "attributedTo": "http://127.0.0.1:9998/actors/4b39b035-e38a-4f79-a3e0-14cc0798fe42",
6 | "content": "A secondthird reply.
\n",
7 | "mediaType": "text/html",
8 | "inReplyTo": [
9 | "http://127.0.0.1:9998/objects/1",
10 | "http://127.0.0.1:9998/objects/2",
11 | "http://127.0.0.1:9998/objects/5"
12 | ],
13 | "published": "2019-09-27T14:26:43.235793852Z",
14 | "updated": "2019-09-27T14:26:43.235793852Z",
15 | "to": [
16 | "https://www.w3.org/ns/activitystreams#Public"
17 | ],
18 | "replies": "http://127.0.0.1:9998/objects/6/replies",
19 | "likes": "http://127.0.0.1:9998/objects/6/likes",
20 | "shares": "http://127.0.0.1:9998/objects/6/shares",
21 | "source": {
22 | "content": "A ~~second~~third reply.",
23 | "mediaType": "text/markdown"
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/note-replyTo-1-and-2.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/objects/5",
4 | "type": "Note",
5 | "attributedTo": "http://127.0.0.1:9998/actors/4b39b035-e38a-4f79-a3e0-14cc0798fe42",
6 | "content": "A second reply.
\n",
7 | "mediaType": "text/html",
8 | "inReplyTo": [
9 | "http://127.0.0.1:9998/objects/1",
10 | "http://127.0.0.1:9998/objects/2"
11 | ],
12 | "published": "2019-09-27T14:26:43.235793852Z",
13 | "updated": "2019-09-27T14:26:43.235793852Z",
14 | "to": [
15 | "https://www.w3.org/ns/activitystreams#Public"
16 | ],
17 | "replies": "http://127.0.0.1:9998/objects/5/replies",
18 | "likes": "http://127.0.0.1:9998/objects/5/likes",
19 | "shares": "http://127.0.0.1:9998/objects/5/shares",
20 | "source": {
21 | "content": "A second reply.",
22 | "mediaType": "text/markdown"
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/note.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/objects/41e7ec45-ff92-473a-b79d-974bf30a0aba",
4 | "type": "Note",
5 | "attributedTo": "http://127.0.0.1:9998/actors/4b39b035-e38a-4f79-a3e0-14cc0798fe42",
6 | "content": "Hello
\nworld
!
\n",
7 | "mediaType": "text/html",
8 | "inReplyTo": [
9 | "http://127.0.0.1:9998/objects/2d365f94-8de4-4681-a7db-7a4b947ad302"
10 | ],
11 | "published": "2019-09-27T14:26:43.235793852Z",
12 | "updated": "2019-09-27T14:26:43.235793852Z",
13 | "url": "/~admin/41e7ec45-ff92-473a-b79d-974bf30a0aba",
14 | "to": [
15 | "https://www.w3.org/ns/activitystreams#Public"
16 | ],
17 | "source": {
18 | "content": "Another first reply.",
19 | "mediaType": "text/markdown"
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/page-2.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/objects/2",
3 | "type": "Page",
4 | "name": "Humble Brag day: mpris-scrobbler",
5 | "attributedTo": "http://127.0.0.1:9998/actors/2",
6 | "url": "https://github.com/mariusor/mpris-scrobbler",
7 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ],
8 | "cc": [ "http://127.0.0.1:9998/actors/2/followers" ],
9 | "replies": "http://127.0.0.1:9998/objects/2/replies",
10 | "likes": "http://127.0.0.1:9998/objects/2/likes",
11 | "shares": "http://127.0.0.1:9998/objects/2/shares"
12 | }
13 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/place-4.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/objects/4",
3 | "type": "Place",
4 | "name": "You are here",
5 | "attributedTo": "http://127.0.0.1:9998/actors/2",
6 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ],
7 | "cc": [ "http://127.0.0.1:9998/actors/2/followers" ],
8 | "shares": "http://127.0.0.1:9998/objects/2/shares",
9 | "replies": "http://127.0.0.1:9998/objects/2/replies",
10 | "likes": "http://127.0.0.1:9998/objects/2/likes",
11 | "accuracy": 0.4,
12 | "altitude": 0,
13 | "latitude": -49.3454,
14 | "longitude": 68.9782,
15 | "radius": 100,
16 | "units": "meters"
17 | }
18 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/tag-mod.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/objects/t1",
3 | "name": "#mod",
4 | "to": ["https://www.w3.org/ns/activitystreams#Public"]
5 | }
6 |
--------------------------------------------------------------------------------
/tests/mocks/c2s/objects/tombstone-3.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.0.1:9998/objects/3",
4 | "formerType": "Object",
5 | "type": "Tombstone",
6 | "attributedTo": "http://127.0.0.1:9998/actors/3",
7 | "to": ["https://www.w3.org/ns/activitystreams#Public"]
8 | }
9 |
--------------------------------------------------------------------------------
/tests/mocks/keys/ed25519.prv:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MC4CAQAwBQYDK2VwBCIEIOTbIkjEpFJ7vW+WyzKnb5bRVYb/5qOjAGmnQ1cEK/JY
3 | -----END PRIVATE KEY-----
4 |
--------------------------------------------------------------------------------
/tests/mocks/keys/ed25519.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MCowBQYDK2VwAyEAyw0F0he2pXCvElvpJmotbEiLpB4PkcBsajUU+n/lfBk=
3 | -----END PUBLIC KEY-----
4 |
--------------------------------------------------------------------------------
/tests/mocks/keys/rsa2048.prv:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmQvcHQBftevun
3 | T611ayR8qHjymTa8Tl4BuFn6POfCgQWvhFpfA+FtsCxSgtpwR/iLBx84NMKYqnCh
4 | 51OZ3TUhaAhO1/mgt/MAeSGdXRBsJ4lP9ytofZ2sN5DMkZ5yB27yjglvFjBJnfct
5 | 9/RBLWpUAgqIyZ5EonP+LyrIyTL27Ja8VFEwBBlYO/wQA8vIBP30+nFdhkuvu5Nf
6 | B6s6+eQl4suPRc2NjH2cBA6KDfM/0PmyCFhVKMeRa5LztJtzU7NhwTU9j44ee5hE
7 | +lHVZcfzzRaIRori/WQWgKXrZQJH3qGZAUe8BzR5MLYQNXieczm4tsXz+nSAzL2Y
8 | 9h9Od8kTAgMBAAECggEABHTz4+EQwyNY1Tm9j9HSpV5rksh1cl00Ox35MeWpqmOE
9 | oEyoxemrHiJq2othCUF+PoiiVzXGDnGczO12jQyKlnd2p/M1YQ/zsHLCA+xKus6u
10 | HG0NYAlKzYlvyD9FdnXcjR2Uz/WFHEDi/C7fzVKtijL3AiB/FAS6BBMfnBV72EKh
11 | EOwjGO3IeYykyDVF6BNETd2vI/yN0xjBZtJ/5QKMEr5ELhUD68xEd2iH6/f0eSiz
12 | egjR0QkLWy/3xCtma72medZkZJg54PT9FUP4SdhQJ+kQ1yLPnlSlGGJ7jSQOSgOK
13 | YBbemrDBlUTppSciwIj/F8Ohn16RCm8yfsksxeOBgQKBgQDTj+R/U9LcinW5z5mG
14 | 4NbXar1UWIjM3+FfFzIKzf+7pd+h23UNk+UGhlZ4pFs7R37shEdu4OZq3zb/xX7L
15 | oFJkaaXDoibuTW6US12Fa+u/HdFVu5+eiIeApkE5GumqH8qc2V6BIPTUj302tG4U
16 | K+6IjZnuXpB7C7ujG5Z9v5vHpQKBgQDJLy0E4ajfaE6ph7Fr/hdc7quYz/4p/uwz
17 | aNb6ugcOjH6oItq3dpiBRiUOeu7OqA/cSN9cIJt8QRvllFXXpLbX5092EWjYxCjQ
18 | HsNkR1lVvkHSfVCbDOY8AKE9xdBpOYf0XYfrrr/qdZPyd6SqrH430abmEWw/Bg1a
19 | qsT/4zgwVwKBgQCvsBb5Bgtdyj3piFTehWjki2ee28b/HAx0gzazck6k7iLArxaN
20 | p/vRZ0338cUxfTYSA+euVGYE6kkqLkAVqZXCfVmDFO1viC4ESHHpkq27kG9+2si7
21 | RnYAiBAx8/+Hn88KYhjw8wVeX6qD+2JOrgzwqWbjZPRmul+gHBDlbHFZYQKBgHiQ
22 | BuCIAtVvAsThhld7O7D7bmXzLxMnq5DbYQl79cKoOzazPHL5ZUcDLC3TSc0aNfcC
23 | zKe++q6prfgUvqSuFsyn15yfrj9IvlSKOvmbMFQL4hIr+uQQBOEsV3RXWR/V1D2C
24 | 13NLk8MDlxeUz19gY3s77lKtWjsie1o8QDZAimmdAoGAZtSE5ubaL8XtSpB8+w4o
25 | 2+voXVuWwG6yAgi8xzRa9z8xcMZR5CYuf3g4xo9+oaAXa+huL17Cuu/hUy1wzfu1
26 | a2bmk0qMFZ+tKBnzzQiK9NXv+FBUdZC9wamkLAjDqMPSl/mKZLjiLrhE1N5WkrRG
27 | lpyauFSLBeY1+n+Xz1zJIpY=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/tests/mocks/keys/rsa2048.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApkL3B0AX7Xr7p0+tdWsk
3 | fKh48pk2vE5eAbhZ+jznwoEFr4RaXwPhbbAsUoLacEf4iwcfODTCmKpwoedTmd01
4 | IWgITtf5oLfzAHkhnV0QbCeJT/craH2drDeQzJGecgdu8o4JbxYwSZ33Lff0QS1q
5 | VAIKiMmeRKJz/i8qyMky9uyWvFRRMAQZWDv8EAPLyAT99PpxXYZLr7uTXwerOvnk
6 | JeLLj0XNjYx9nAQOig3zP9D5sghYVSjHkWuS87Sbc1OzYcE1PY+OHnuYRPpR1WXH
7 | 880WiEaK4v1kFoCl62UCR96hmQFHvAc0eTC2EDV4nnM5uLbF8/p0gMy9mPYfTnfJ
8 | EwIDAQAB
9 | -----END PUBLIC KEY-----
10 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/accept-follow.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/activities/2",
3 | "type": "Accept",
4 | "actor": "{{ .ActorID }}",
5 | "object": "{{ .ObjectID }}",
6 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/activities/accept-follow-666-johndoe.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.0.1:9998/activities/2",
3 | "type": "Accept",
4 | "actor": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1",
5 | "object": "http://127.0.2.1:9999/activities/2",
6 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/activities/create-actor-666.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.2.1:9999/activities/1",
3 | "type": "Create",
4 | "actor": "http://127.0.2.1:9999/actors/666",
5 | "object": "http://127.0.2.1:9999/actors/666",
6 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/activities/create-note-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.2.1:9999/activities/1",
3 | "type": "Create",
4 | "actor": "http://127.0.2.1:9999/actors/666",
5 | "object": "http://127.0.2.1:9999/objects/1",
6 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/activities/follow-666-johndoe.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.2.1:9999/activities/2",
3 | "type": "Follow",
4 | "attributedTo": "http://127.0.2.1/actors/666",
5 | "to": [
6 | "https://www.w3.org/ns/activitystreams#Public",
7 | "http://127.0.0.1/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1"
8 | ],
9 | "published": "2022-07-24T14:41:04Z",
10 | "actor": "http://127.0.2.1:9999/actors/666",
11 | "object": "http://127.0.0.1:9998/actors/e869bdca-dd5e-4de7-9c5d-37845eccc6a1"
12 | }
13 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/activities/follow-mitra.json:
--------------------------------------------------------------------------------
1 | {"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1","https://w3id.org/security/data-integrity/v1",{"Hashtag":"as:Hashtag","MitraJcsRsaSignature2022":"mitra:MitraJcsRsaSignature2022","mitra":"http://jsonld.mitra.social#","proofPurpose":"sec:proofPurpose","proofValue":"sec:proofValue","sensitive":"as:sensitive","verificationMethod":"sec:verificationMethod"}],"actor":"http://127.0.2.1:9999/actors/mitraUser","id":"http://127.0.2.1:9999/objects/018b86be-2d33-ea98-6537-37d50fc19c76","object":"https://federated.id/actors/8b740680-ccb4-4265-82e7-4ac2ca402750","to":["https://federated.id/actors/8b740680-ccb4-4265-82e7-4ac2ca402750"],"type":"Follow"}
2 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/actors/actor-666.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.2.1:9999/actors/666",
4 | "type": "Person",
5 | "attributedTo": "http://127.0.2.1:9999",
6 | "audience": ["https://www.w3.org/ns/activitystreams#Public"],
7 | "generator": "http://127.0.2.1:9999",
8 | "inbox": "http://127.0.2.1:9999/actors/666/inbox",
9 | "outbox": "http://127.0.2.1:9999/actors/666/outbox",
10 | "following": "http://127.0.2.1:9999/actors/666/following",
11 | "followers": "http://127.0.2.1:9999/actors/666/followers",
12 | "preferredUsername": "lou",
13 | "name": "Loucien Cypher",
14 | "endpoints": {
15 | "oauthAuthorizationEndpoint": "http://127.0.2.1:9999/oauth/authorize",
16 | "oauthTokenEndpoint": "http://127.0.2.1:9999/oauth/token",
17 | "sharedInbox": "http://127.0.2.1:9999/inbox"
18 | },
19 | "publicKey": {
20 | "id": "http://127.0.2.1:9999/actors/666#main-key",
21 | "owner": "http://127.0.2.1:9999/actors/666",
22 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAyw0F0he2pXCvElvpJmotbEiLpB4PkcBsajUU+n/lfBk=\n-----END PUBLIC KEY-----"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/actors/mitra-user.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": [
3 | "https://www.w3.org/ns/activitystreams",
4 | "https://w3id.org/security/v1",
5 | "https://w3id.org/security/data-integrity/v1",
6 | "https://w3id.org/security/multikey/v1",
7 | {
8 | "IdentityProof": "toot:IdentityProof",
9 | "PropertyValue": "schema:PropertyValue",
10 | "VerifiableIdentityStatement": "mitra:VerifiableIdentityStatement",
11 | "featured": "toot:featured",
12 | "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
13 | "mitra": "http://jsonld.mitra.social#",
14 | "schema": "http://schema.org/",
15 | "subject": "mitra:subject",
16 | "subscribers": "mitra:subscribers",
17 | "toot": "http://joinmastodon.org/ns#",
18 | "value": "schema:value"
19 | }
20 | ],
21 | "id": "http://127.0.2.1:9999/actors/mitraUser",
22 | "type": "Person",
23 | "name": "mitraUser",
24 | "bcc": ["https://www.w3.org/ns/activitystreams#Public"],
25 | "preferredUsername": "mitraUser",
26 | "inbox": "http://127.0.2.1:9999/actors/mitraUser/inbox",
27 | "outbox": "http://127.0.2.1:9999/actors/mitraUser/outbox",
28 | "followers": "http://127.0.2.1:9999/actors/mitraUser/followers",
29 | "following": "http://127.0.2.1:9999/actors/mitraUser/following",
30 | "subscribers": "http://127.0.2.1:9999/actors/mitraUser/subscribers",
31 | "featured": "http://127.0.2.1:9999/actors/mitraUser/collections/featured",
32 | "assertionMethod": [
33 | {
34 | "id": "http://127.0.2.1:9999/actors/mitraUser#main-key",
35 | "type": "Multikey",
36 | "controller": "http://127.0.2.1:9999/actors/mitraUser",
37 | "publicKeyMultibase": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
38 | },
39 | {
40 | "id": "http://127.0.2.1:9999/actors/mitraUser#ed25519-key",
41 | "type": "Multikey",
42 | "controller": "http://127.0.2.1:9999/actors/mitraUser",
43 | "publicKeyMultibase": "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS"
44 | }
45 | ],
46 | "authentication": [
47 | {
48 | "id": "http://127.0.2.1:9999/actors/mitraUser#main-key",
49 | "type": "Multikey",
50 | "controller": "http://127.0.2.1:9999/actors/mitraUser",
51 | "publicKeyMultibase": "kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk"
52 | },
53 | {
54 | "id": "http://127.0.2.1:9999/actors/mitraUser#ed25519-key",
55 | "type": "Multikey",
56 | "controller": "http://127.0.2.1:9999/actors/mitraUser",
57 | "publicKeyMultibase": "999999999999999999999999999999999999999999999999"
58 | }
59 | ],
60 | "publicKey": {
61 | "id": "http://127.0.2.1:9999/actors/mitraUser#main-key",
62 | "owner": "http://127.0.2.1:9999/actors/mitraUser",
63 | "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPYL2Hq1CoUsL/ueckFZeDBbY583zkC8ayqlts/efis7s0qyBc+zJm7pSnei3Oe7PaMQGBN1I/WuARtMF60B1F0CAwEAAQ==\n-----END PUBLIC KEY-----"
64 | },
65 | "icon": {
66 | "type": "Image",
67 | "url": "http://127.0.2.1:9999/media/6555555555555555555555555aa49156a499ac30fd1e402f79e7e164adb36e2c.png",
68 | "mediaType": "image/png"
69 | },
70 | "summary": "User of ActivityPub-based micro-blogging and content subscription platform Mitra.",
71 | "alsoKnownAs": [],
72 | "attachment": [
73 | {
74 | "alsoKnownAs": "http://127.0.2.1:9999/actors/mitraUser",
75 | "proof": {
76 | "created": "2022-10-19T01:59:13.666995181Z",
77 | "proofPurpose": "assertionMethod",
78 | "proofValue": "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK",
79 | "type": "MitraJcsEip191Signature2022",
80 | "verificationMethod": "did:pkh:eip155:1:0xdddddddddddddddddddddddddddddddddddddddd"
81 | },
82 | "subject": "did:pkh:eip155:1:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
83 | "type": "VerifiableIdentityStatement"
84 | },
85 | {
86 | "alsoKnownAs": "http://127.0.2.1:9999/actors/mitraUser",
87 | "proof": {
88 | "created": "2021-12-01:55:06.044135547Z",
89 | "cryptosuite": "jcs-eddsa-2022",
90 | "proofPurpose": "assertionMethod",
91 | "proofValue": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
92 | "type": "DataIntegrityProof",
93 | "verificationMethod": "did:key:999999999999999999999999999999999999999999999999"
94 | },
95 | "subject": "did:key:z6MkrJ4F3pUkVE28caQ1LNhUmMHakZsx3GLg2eY666Dv9111",
96 | "type": "VerifiableIdentityStatement"
97 | },
98 | {
99 | "href": "http://127.0.2.1:9999/actors/mitraUser/proposals/monero:33333333333333333333333333333333",
100 | "mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
101 | "name": "MoneroSubscription",
102 | "rel": [
103 | "payment",
104 | "https://w3id.org/valueflows/Proposal"
105 | ],
106 | "type": "Link"
107 | },
108 | {
109 | "name": "Code",
110 | "type": "PropertyValue",
111 | "value": "https://codeberg.org/mitraUser/"
112 | },
113 | {
114 | "name": "$XMR",
115 | "type": "PropertyValue",
116 | "value": "88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888"
117 | },
118 | {
119 | "name": "XMR subscription",
120 | "type": "PropertyValue",
121 | "value": "http://127.0.2.1:9999/@mitraUser/subscription"
122 | },
123 | {
124 | "name": "PGP",
125 | "type": "PropertyValue",
126 | "value": "6666 6666 6666 6666 6666 6666 6666 6666 6666 25F0"
127 | },
128 | {
129 | "name": "Matrix (backup)",
130 | "type": "PropertyValue",
131 | "value": "@mitraUser:matrix.irc"
132 | }
133 | ],
134 | "manuallyApprovesFollowers": false,
135 | "url": "http://127.0.2.1:9999/actors/mitraUser"
136 | }
--------------------------------------------------------------------------------
/tests/mocks/s2s/create-object.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "http://127.0.2.1:9999/activities/1",
3 | "type": "Create",
4 | "actor": "{{ .ActorID }}",
5 | "object": "{{ .ObjectID }}",
6 | "to": [ "https://www.w3.org/ns/activitystreams#Public" ]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/mocks/s2s/objects/note-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "@context": "https://www.w3.org/ns/activitystreams",
3 | "id": "http://127.0.2.1:9999/objects/1",
4 | "type": "Note",
5 | "attributedTo": "http://127.0.2.1:9999/actors/666",
6 | "content": "Hello
\nworld
!
\n",
7 | "mediaType": "text/html",
8 | "url": "https://example.com/1",
9 | "to": ["https://www.w3.org/ns/activitystreams#Public"],
10 | "source": {
11 | "content": "Hello\n\n`world`!",
12 | "mediaType": "text/markdown"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/s2s_test.go:
--------------------------------------------------------------------------------
1 | //go:build integration && s2s
2 |
3 | package tests
4 |
5 | import (
6 | "fmt"
7 | "net/http"
8 | "path"
9 | "testing"
10 |
11 | vocab "github.com/go-ap/activitypub"
12 | )
13 |
14 | func CreateS2SObject(actor *testAccount, object any) actS2SMock {
15 | id := "http://" + s2shost + "/" + path.Join("activities", fmt.Sprintf("%d", activityCount))
16 | var objectId string
17 | switch ob := object.(type) {
18 | case string:
19 | objectId = ob
20 | case *testAccount:
21 | objectId = ob.ID
22 | case vocab.Item:
23 | objectId = string(ob.GetID())
24 | }
25 | return actS2SMock{
26 | Id: id,
27 | ActorID: actor.ID,
28 | ObjectID: objectId,
29 | }
30 | }
31 |
32 | // Generate test Accept with C2S account as actor and the Follow request as object
33 | var generatedAccept = CreateS2SObject(
34 | defaultC2SAccount(),
35 | loadMockFromDisk("mocks/s2s/activities/follow-666-johndoe.json", nil),
36 | )
37 |
38 | // S2SReceiveTests builds tests for verifying a FedBOX instance receives and processes correctly
39 | // activities coming from federated requests.
40 | var S2SReceiveTests = testPairs{
41 | {
42 | name: "CreateActor",
43 | configs: s2sConfigs,
44 | tests: []testPair{
45 | {
46 | mocks: []string{
47 | "mocks/c2s/actors/service.json",
48 | "mocks/c2s/actors/actor-johndoe.json",
49 | "mocks/c2s/actors/application.json",
50 | // S2S objects need to be present
51 | "mocks/s2s/activities/create-actor-666.json",
52 | "mocks/s2s/actors/actor-666.json",
53 | },
54 | req: testReq{
55 | met: http.MethodPost,
56 | account: defaultS2SAccount(),
57 | urlFn: InboxURL(defaultC2SAccount()),
58 | bodyFn: loadMockJson(
59 | "mocks/s2s/create-object.json",
60 | CreateS2SObject(defaultS2SAccount(), defaultS2SAccount()),
61 | ),
62 | },
63 | res: testRes{
64 | code: http.StatusCreated,
65 | val: &objectVal{
66 | typ: string(vocab.CreateType),
67 | act: &objectVal{
68 | id: defaultS2SAccount().ID,
69 | typ: string(vocab.PersonType),
70 | preferredUsername: "lou",
71 | },
72 | obj: &objectVal{
73 | id: defaultS2SAccount().ID,
74 | typ: string(vocab.PersonType),
75 | preferredUsername: "lou",
76 | name: "Loucien Cypher",
77 | },
78 | },
79 | },
80 | },
81 | },
82 | },
83 | {
84 | name: "CreateNote",
85 | configs: s2sConfigs,
86 | tests: []testPair{
87 | {
88 | mocks: []string{
89 | "mocks/c2s/actors/service.json",
90 | "mocks/c2s/actors/actor-johndoe.json",
91 | "mocks/c2s/actors/application.json",
92 | // s2s entities that need to exist
93 | "mocks/s2s/actors/actor-666.json",
94 | "mocks/s2s/objects/note-1.json",
95 | "mocks/s2s/activities/create-note-1.json",
96 | },
97 | req: testReq{
98 | met: http.MethodPost,
99 | account: defaultS2SAccount(),
100 | urlFn: InboxURL(defaultC2SAccount()),
101 | bodyFn: loadMockJson(
102 | "mocks/s2s/create-object.json",
103 | CreateS2SObject(defaultS2SAccount(), loadMockFromDisk("mocks/s2s/objects/note-1.json", nil)),
104 | ),
105 | },
106 | res: testRes{
107 | code: http.StatusCreated,
108 | val: &objectVal{
109 | typ: string(vocab.CreateType),
110 | act: &objectVal{
111 | id: defaultS2SAccount().ID,
112 | typ: string(vocab.PersonType),
113 | preferredUsername: "lou",
114 | name: "Loucien Cypher",
115 | },
116 | obj: &objectVal{
117 | id: loadMockFromDisk("mocks/s2s/objects/note-1.json", nil).GetID().String(),
118 | typ: string(loadMockFromDisk("mocks/s2s/objects/note-1.json", nil).GetType()),
119 | },
120 | },
121 | },
122 | },
123 | },
124 | },
125 | {
126 | name: "AcceptFollow",
127 | configs: s2sConfigs,
128 | tests: []testPair{
129 | {
130 | mocks: []string{
131 | "mocks/c2s/actors/service.json",
132 | "mocks/c2s/actors/actor-johndoe.json",
133 | "mocks/c2s/actors/application.json",
134 | // s2s entities that need to exist
135 | "mocks/s2s/actors/actor-666.json",
136 | "mocks/s2s/activities/follow-666-johndoe.json",
137 | // This is used for validation
138 | "mocks/s2s/activities/accept-follow-666-johndoe.json",
139 | },
140 | req: testReq{
141 | met: http.MethodPost,
142 | account: defaultC2SAccount(),
143 | urlFn: InboxURL(defaultS2SAccount()),
144 | bodyFn: loadMockJson(
145 | "mocks/s2s/accept-follow.json",
146 | generatedAccept,
147 | ),
148 | },
149 | res: testRes{
150 | code: http.StatusCreated,
151 | val: &objectVal{
152 | typ: string(vocab.AcceptType),
153 | act: &objectVal{
154 | id: defaultC2SAccount().ID,
155 | typ: string(vocab.PersonType),
156 | preferredUsername: defaultC2SAccount().Handle,
157 | },
158 | obj: &objectVal{
159 | id: generatedAccept.ObjectID,
160 | typ: string(vocab.FollowType),
161 | obj: &objectVal{
162 | id: defaultC2SAccount().ID,
163 | typ: string(vocab.PersonType),
164 | },
165 | act: &objectVal{
166 | id: defaultS2SAccount().ID,
167 | },
168 | },
169 | },
170 | },
171 | },
172 | {
173 | // The followers collection doesn't really exist because we didn't mock it
174 | req: testReq{
175 | met: http.MethodGet,
176 | urlFn: FollowersURL(defaultC2SAccount()),
177 | },
178 | res: testRes{
179 | code: http.StatusNotFound,
180 | },
181 | },
182 | {
183 | req: testReq{
184 | met: http.MethodGet,
185 | urlFn: FollowingURL(defaultS2SAccount()),
186 | },
187 | res: testRes{
188 | code: http.StatusOK,
189 | val: &objectVal{
190 | id: CollectionURL(FollowingURL(defaultS2SAccount())(), firstPage()),
191 | typ: string(vocab.OrderedCollectionPageType),
192 | itemCount: 1,
193 | items: map[string]*objectVal{},
194 | },
195 | },
196 | },
197 | },
198 | },
199 | }
200 |
201 | func Test_S2SReceiveRequests(t *testing.T) {
202 | runTestSuites(t, S2SReceiveTests)
203 | }
204 |
--------------------------------------------------------------------------------
/tests/script.js:
--------------------------------------------------------------------------------
1 | import {check, group, sleep} from 'k6';
2 | import http from 'k6/http';
3 | import {Rate} from 'k6/metrics';
4 |
5 | const errors = new Rate('error_rate');
6 |
7 | export const options = {
8 | thresholds: {
9 | 'http_req_duration': ['p(95)<200'],
10 | 'error_rate': [{threshold: 'rate < 0.01', abortOnFail: true, delayAbortEval: '1s'}],
11 | 'error_rate{errorType:responseStatusError}': [{threshold: 'rate < 0.1'}],
12 | 'error_rate{errorType:contentTypeError}': [{threshold: 'rate < 0.1'}],
13 | 'error_rate{errorType:bodySizeError}': [{threshold: 'rate < 0.1'}],
14 | 'error_rate{errorType:ActivityPubError}': [{threshold: 'rate < 0.1'}],
15 | },
16 | scenarios: {
17 | slam: {
18 | executor: 'ramping-arrival-rate',
19 | exec: 'slam',
20 | startRate: 0,
21 | timeUnit: '30s',
22 | preAllocatedVUs: 100,
23 | maxVUs: 100,
24 | stages: [
25 | { target: 200, duration: '120s'},
26 | { target: 20, duration: '10s'},
27 | ],
28 | },
29 | },
30 | maxRedirects: 0,
31 | }
32 |
33 | const BASE_URL = __ENV.TEST_HOST;
34 |
35 | const actors = [
36 | {
37 | id: `${BASE_URL}/`,
38 | name: 'self',
39 | type: 'Service',
40 | }
41 | ]
42 |
43 | export function setup() {
44 | // Do not try to run the setup on remote servers
45 | if (!BASE_URL.endsWith('.local')) return;
46 |
47 | for (let u of actors) {
48 | if (u.hasOwnProperty('id')) {
49 | // actor exists
50 | check(http.get(u.id), {'is ActivityPub': isActivityPub});
51 | } else {
52 |
53 | }
54 | }
55 | }
56 |
57 | function ActivityPubChecks() {
58 | return {
59 | 'status 200': isOK,
60 | 'is ActivityPub': isActivityPub,
61 | }
62 | }
63 |
64 | function hasActivityPubType(types) {
65 | if (!Array.isArray(types )) types = [types];
66 | return (r) => {
67 | const status = types.findIndex((e) => e === r.json('type')) > 0;
68 | errors.add(!status, {errorType: 'ActivityPubError'});
69 | return status
70 | }
71 | }
72 |
73 | function CollectionChecks() {
74 | return {
75 | 'has correct Type': hasActivityPubType(['Collection', 'OrderedCollection']),
76 | }
77 | }
78 |
79 | function CollectionPageChecks() {
80 | return {
81 | 'has correct Type': hasActivityPubType(['CollectionPage', 'OrderedCollectionPage']),
82 | }
83 | }
84 |
85 | function isOK(r) {
86 | const status = r.status === 200;
87 | errors.add(!status, {errorType: 'responseStatusError'});
88 | return status;
89 | }
90 |
91 | function isActivityPub(r) {
92 | const ct = contentType(r);
93 | const contentTypeStatus = (
94 | ct.startsWith('application/json')
95 | || ct.startsWith('application/activity+json')
96 | || ct.startsWith('application/ld+json')
97 | );
98 | const bodyLengthStatus = r.body.length > 0;
99 |
100 | errors.add(!contentTypeStatus, {errorType: 'contentTypeError'});
101 | errors.add(!bodyLengthStatus, {errorType: 'bodySizeError'});
102 | return contentTypeStatus && bodyLengthStatus;
103 | }
104 |
105 | function actorChecks(u) {
106 | return Object.assign(
107 | ActivityPubChecks(),
108 | isSpecificActor(u),
109 | );
110 | }
111 |
112 | function collectionChecks() {
113 | return Object.assign(
114 | ActivityPubChecks(),
115 | CollectionChecks(),
116 | );
117 | }
118 |
119 | function collectionPageChecks() {
120 | return Object.assign(
121 | ActivityPubChecks(),
122 | CollectionPageChecks(),
123 | );
124 | }
125 |
126 | function isSpecificActor(u) {
127 | let result = {
128 | 'has body': (r) => r.body.length > 0,
129 | };
130 | for (let prop in u) {
131 | const propName = `property ${prop.toUpperCase()}`;
132 | result[propName] = (r) => {
133 | const ob = r.json();
134 | return !ob.hasOwnProperty(prop) || ob[prop] === u[prop]
135 | };
136 | }
137 | return result;
138 | }
139 |
140 | function getHeader(hdr) {
141 | return (r) => r.headers.hasOwnProperty(hdr) ? r.headers[hdr].toLowerCase() : '';
142 | }
143 |
144 | const contentType = getHeader('Content-Type');
145 |
146 | const objectCollections = ['likes', 'shares', 'replies'];
147 | const actorCollections = ['inbox', 'outbox', 'following', 'followers', 'liked', 'likes', 'shares', 'replies'];
148 |
149 | function aggregateActorCollections(actor) {
150 | let collections = [];
151 | for (let i in actorCollections) {
152 | const col = actorCollections[i];
153 | if (actor.hasOwnProperty(col)) {
154 | collections.push(actor[col])
155 | }
156 | }
157 | if (actor.hasOwnProperty('streams')) {
158 | collections.push(...actor['streams'])
159 | }
160 | return collections;
161 | }
162 |
163 | function runSuite(actors, sleepTime = 0) {
164 | return () => {
165 | for (let u of actors) {
166 | if (!u.hasOwnProperty('id')) {
167 | console.error('invalid actor to test, missing "id" property');
168 | continue;
169 | }
170 | group(u.id, function () {
171 | const r = http.get(u.id)
172 | check(r, actorChecks(u));
173 |
174 | const actor = r.json();
175 | group('collections', function () {
176 | for (const colIRI of aggregateActorCollections(actor)) {
177 | group(colIRI, function () {
178 | const r = http.get(colIRI)
179 | check(r, collectionChecks());
180 |
181 | let col = r.json();
182 | let next, pageCount = 0;
183 |
184 | if (col.hasOwnProperty('first') && col['first'] !== col['id']) {
185 | next = col['first']
186 | }
187 | while (true) {
188 | if (col.hasOwnProperty('next') && col['next'] !== col['id']) {
189 | next = col['next']
190 | }
191 | if (next === '') break;
192 |
193 | group(`[${pageCount}]${next}`, function () {
194 | const r = http.get(next)
195 | !check(r, collectionPageChecks());
196 | col = r.json();
197 | });
198 | next = '';
199 | pageCount++;
200 | }
201 | });
202 | }
203 | });
204 | sleep(sleepTime);
205 | });
206 | }
207 | }
208 | }
209 |
210 | export function slam() {
211 | group('actors', runSuite(actors));
212 | }
213 |
--------------------------------------------------------------------------------
/tools/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | _ctl=./bin/fedboxctl
4 | _env=${1}
5 |
6 | if ! expect -v &> /dev/null ; then
7 | echo "Unable to find 'expect' command, which is required"
8 | exit 1
9 | fi
10 |
11 | _ENV_FILE="./.env"
12 | if [[ ! -f ${_ENV_FILE} ]]; then
13 | _ENV_FILE="./.env.${_env}"
14 | fi
15 | if [ ! -f "${_ENV_FILE}" ]; then
16 | echo "Invalid configuration file ${_ENV_FILE}"
17 | exit 1
18 | fi
19 |
20 | source ${_ENV_FILE}
21 |
22 | if [[ -z "${FEDBOX_HOSTNAME}" ]]; then
23 | FEDBOX_HOSTNAME=${HOSTNAME}
24 | fi
25 | if [[ -z "${FEDBOX_HOSTNAME}" ]]; then
26 | echo "Missing fedbox hostname in environment";
27 | exit 1
28 | fi
29 | if [[ -z "${OAUTH2_SECRET}" ]]; then
30 | echo "Missing OAuth2 secret in environment";
31 | exit 1
32 | fi
33 | if [[ -z "${OAUTH2_CALLBACK_URL}" ]]; then
34 | echo "Missing OAuth2 callback url in environment";
35 | exit 1
36 | fi
37 |
38 | _FULL_PATH="${STORAGE_PATH}/${ENV}/${FEDBOX_HOSTNAME}"
39 | if [[ -d "${_FULL_PATH}" ]]; then
40 | echo "skipping bootstrapping ${_FULL_PATH}"
41 | else
42 | # create storage
43 | ${_ctl} bootstrap
44 | fi
45 |
46 | _HAVE_OAUTH2_SECRET=$(grep OAUTH2_SECRET "${_ENV_FILE}" | cut -d'=' -f2 | tail -n1)
47 | _HAVE_OAUTH2_CLIENT=$(${_ctl} oauth client ls | grep -c "${OAUTH2_KEY}")
48 |
49 | if [[ ${_HAVE_OAUTH2_CLIENT} -ge 1 && "z${_HAVE_OAUTH2_SECRET}" == "z${OAUTH2_SECRET}" ]]; then
50 | echo "skipping adding OAuth2 client"
51 | else
52 | # add oauth2 client for Brutalinks
53 | echo OAUTH2_APP=$(./tools/clientadd.sh "${OAUTH2_SECRET}" "${OAUTH2_CALLBACK_URL}" | grep Client | tail -1 | awk '{print $3}')
54 | echo OAUTH2_SECRET="${OAUTH2_SECRET}"
55 | fi
56 |
57 | _ADMIN_NAME=admin
58 | _HAVE_ADMIN=$(${_ctl} ap ls --type Person | jq -r .[].preferredUsername | grep -c "${_ADMIN_NAME}")
59 | if [[ ${_HAVE_ADMIN} -ge 1 ]]; then
60 | echo "skipping adding user ${_ADMIN_NAME}"
61 | else
62 | if [[ -n "${ADMIN_PW}" ]]; then
63 | # add admin user for Brutalinks
64 | ./tools/useradd.sh "${_ADMIN_NAME}" "${ADMIN_PW}"
65 | fi
66 | fi
67 |
--------------------------------------------------------------------------------
/tools/build-images.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -ex
4 |
5 | make -C images builder
6 | _storage=${1:-all}
7 | _push=${2:-build}
8 |
9 | _sha=$(git rev-parse --short HEAD)
10 | _branch=$(git branch --show-current)
11 | make -C images STORAGE="${_storage}" ENV=dev VERSION="${_branch}" ${_push}
12 | if [ "${_branch}" = "master" ]; then
13 | _branch=$(printf "%s-%s" "${_branch}" "${_sha}")
14 | make -C images STORAGE="${_storage}" ENV=qa VERSION="${_branch}" ${_push}
15 | fi
16 |
17 | _tag=$(git describe --long --tags || true)
18 | if [ -n "${_tag}" ]; then
19 | make -C images STORAGE="${_storage}" ENV=prod VERSION="${_tag}" ${_push}
20 | fi
21 |
22 |
--------------------------------------------------------------------------------
/tools/clientadd.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/expect -f
2 | # Special thanks to github.com/squash for providing this script in the course of solving
3 | # https://github.com/mariusor/go-littr/issues/38#issuecomment-658800183
4 |
5 | set pass [lindex $argv 0]
6 | set callback_url [lindex $argv 1]
7 |
8 | spawn ./bin/fedboxctl oauth client add --redirectUri "${callback_url}"
9 | expect "client's pw: "
10 | send "${pass}\r"
11 | expect "pw again: "
12 | send "${pass}\r"
13 | expect eof
14 |
15 |
--------------------------------------------------------------------------------
/tools/run-container:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | _path=${1}
4 | _what=${2}
5 |
6 | #set -ex
7 | #set -e
8 | if [ -z "${_path}" ]; then
9 | echo "you must pass the path where to run the container"
10 | exit 1
11 | fi
12 |
13 | if [ -z "${_what}" ]; then
14 | echo "you must pass the container image to run"
15 | exit 1
16 | fi
17 |
18 | _storage=$(realpath ${_path})
19 | if [ ! -d ${_storage} ]; then
20 | echo "Storage path is not accessible ${_storage}"
21 | exit 1
22 | fi
23 |
24 | _env=$(find ${_storage} -iname ".env*" | tail -n1)
25 | echo $_env
26 | if [ ! -f ${_env} ]; then
27 | echo "env file is not accessible in path ${_storage}"
28 | exit 1
29 | fi
30 | _name=$(grep HOSTNAME ${_env} | tail -n1 | cut -d'=' -f 2 | cut -d':' -f 2)
31 | _port=$(grep LISTEN ${_env} | tail -n1 | cut -d'=' -f 2 | cut -d':' -f 2)
32 |
33 | CMD=$(command -v podman || which docker)
34 |
35 | $CMD run --pull=newer --network=host --name=${_name} --replace -v ${_env}:/.env -v ${_storage}:/storage --env-file=${_env} ${_what}
36 |
--------------------------------------------------------------------------------
/tools/run-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | RED='\033[0;31m'
4 | GREEN='\033[1;32m'
5 | YELLOW='\033[1;33m'
6 | NC='\033[0m' # No Color
7 |
8 | run_tests() {
9 | _storage=${1}
10 | echo -e "Testing ${RED}C2S${NC} ${GREEN}${_storage}${NC} with CGO ${YELLOW}Disabled${NC}"
11 | make STORAGE="${_storage}" CGO_ENABLED=0 TEST_FLAGS='-count=1 -cover' -C tests c2s
12 | echo -e "Testing ${RED}S2S${NC} ${GREEN}${_storage}${NC} with CGO ${YELLOW}Disabled${NC}"
13 | make STORAGE="${_storage}" CGO_ENABLED=0 TEST_FLAGS='-count=1 -cover' -C tests s2s
14 | echo -e "Testing ${RED}C2S${NC} ${GREEN}all_${_storage}${NC} and CGO ${YELLOW}Disabled${NC}"
15 | make FEDBOX_STORAGE="${_storage}" CGO_ENABLED=0 TEST_FLAGS='-count=1 -cover' -C tests c2s
16 | echo -e "Testing ${RED}S2S${NC} ${GREEN}all_${_storage}${NC} and CGO ${YELLOW}Disabled${NC}"
17 | make FEDBOX_STORAGE="${_storage}" CGO_ENABLED=0 TEST_FLAGS='-count=1 -cover' -C tests s2s
18 | echo -e "Testing ${RED}C2S${NC} ${GREEN}${_storage}${NC} with CGO ${YELLOW}Enabled${NC}"
19 | make STORAGE="${_storage}" CGO_ENABLED=1 TEST_FLAGS='-race -count=1' -C tests c2s
20 | echo -e "Testing ${RED}S2S${NC} ${GREEN}${_storage}${NC} with CGO ${YELLOW}Enabled${NC}"
21 | make STORAGE="${_storage}" CGO_ENABLED=1 TEST_FLAGS='-race -count=1' -C tests s2s
22 | echo -e "Testing ${RED}C2S${NC} ${GREEN}all_${_storage}${NC} with CGO ${YELLOW}Enabled${NC}"
23 | make FEDBOX_STORAGE="${_storage}" CGO_ENABLED=1 TEST_FLAGS='-race -count=1' -C tests c2s
24 | echo -e "Testing ${RED}S2S${NC} ${GREEN}all_${_storage}${NC} with CGO ${YELLOW}Enabled${NC}"
25 | make FEDBOX_STORAGE="${_storage}" CGO_ENABLED=1 TEST_FLAGS='-race -count=1' -C tests s2s
26 | echo ""
27 | }
28 |
29 | if [[ "${1}" = "" ]]; then
30 | #_tests=(fs sqlite boltdb badger)
31 | _tests=(fs sqlite boltdb)
32 | else
33 | _tests="${@}"
34 | fi
35 |
36 | for _test in ${_tests[@]} ; do
37 | run_tests "${_test}"
38 | done
39 |
40 | find ./tests/.cache/ -mindepth 1 -type d -exec rm -rf {} +
41 |
--------------------------------------------------------------------------------
/tools/update.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -xe
3 |
4 | deps=(activitypub auth client errors jsonld processing storage-fs storage-sqlite storage-boltdb storage-badger)
5 |
6 | for dep in ${deps[@]}; do
7 | sha=$(git --git-dir="../go-ap/${dep}/.git" log -n1 --format=tformat:%h)
8 | go get -u github.com/go-ap/${dep}@${sha}
9 | done
10 |
11 | deps=(wrapper lw)
12 | for dep in ${deps[@]}; do
13 | sha=$(git --git-dir="../${dep}/.git" log -n1 --format=tformat:%h)
14 | go get -u git.sr.ht/~mariusor/${dep}@${sha}
15 | done
16 |
17 | deps=(render)
18 | for dep in ${deps[@]}; do
19 | sha=$(git --git-dir="../${dep}/.git" log -n1 --format=tformat:%h)
20 | go get -u github.com/mariusor/${dep}@${sha}
21 | done
22 | go mod tidy
23 |
24 | make test
25 |
26 | set +e
27 | #ake STORAGE=fs integration
28 | #ake STORAGE=boltdb integration
29 | #ake STORAGE=badger integration
30 | #ake STORAGE=sqlite integration
31 |
--------------------------------------------------------------------------------
/tools/useradd.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/expect -f
2 | # Special thanks to github.com/squash for providing this script in the course of solving
3 | # https://github.com/mariusor/go-littr/issues/38#issuecomment-658800183
4 |
5 | set name [lindex $argv 0]
6 | set pass [lindex $argv 1]
7 |
8 | spawn ./bin/fedboxctl ap actor add ${name}
9 | expect "${name}'s pw: "
10 | send "${pass}\r"
11 | expect "pw again: "
12 | send "${pass}\r"
13 | expect eof
14 |
15 |
--------------------------------------------------------------------------------