├── .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 | [![MIT Licensed](https://img.shields.io/github/license/go-ap/fedbox.svg)](https://raw.githubusercontent.com/go-ap/fedbox/master/LICENSE) 4 | [![Build Status](https://builds.sr.ht/~mariusor/fedbox.svg)](https://builds.sr.ht/~mariusor/fedbox) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-ap/fedbox)](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 |
  1. {{$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 |
  2. 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 |

Fed::BOX

12 | {{- $handle := .Handle -}} 13 |
14 |
15 | {{/*
*/}} 16 | {{/* Local authentication*/}} 17 | 18 | 19 |
20 |
21 |
22 |
23 | 24 | {{/*
*/}} 25 |
26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /internal/assets/templates/password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{.Title}} 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Fed::BOX

12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 |
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

\n

world!

\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

\n

world!

\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

\n

world!

\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 | --------------------------------------------------------------------------------