diff --git a/FEDERATION.md b/FEDERATION.md index b6e923ac..4d7a0aa4 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -57,17 +57,17 @@ By default, tootik uses `draft-cavage-http-signatures` when it signs outgoing re ## Application Actor -tootik creates a special user named `nobody`, which acts as an [Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md). Its key is used to sign outgoing requests not initiated by a particular user. +tootik creates a special user named `actor`, which acts as an [Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md). Its key is used to sign outgoing requests not initiated by a particular user. There are multiple ways to "discover" this actor: 1. Using [WebFinger](https://www.rfc-editor.org/rfc/rfc7033), just like any other user: ```sh -curl https://example.org/.well-known/webfinger?resource=acct:nobody@example.org +curl https://example.org/.well-known/webfinger?resource=acct:actor@example.org ``` -2. For compatibility with servers that allow discovery of the Application Actor, the domain is an alias of `nobody`: +2. For compatibility with servers that allow discovery of the Application Actor, the domain is an alias of `actor`: ```sh curl https://example.org/.well-known/webfinger?resource=acct:example.org@example.org @@ -85,15 +85,15 @@ curl -H "accept: application/activity+json" https://example.org curl https://example.org/actor ``` -5. The `links` array returned by `/.well-known/nodeinfo` links to `nobody`, as [FEP-2677](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md) requires +5. The `links` array returned by `/.well-known/nodeinfo` links to `actor`, as [FEP-2677](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md) requires ```sh curl https://example.org/.well-known/nodeinfo ``` -The `sharedInbox` of non-portable actors points to `nobody`'s inbox, to reduce the number of requests from servers that deduplicate outgoing requests by `sharedInbox` during wide delivery of posts. +`actor` advertises support for RFC9421 and Ed25519 using [FEP-844e](https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md), to encourage other servers to use these capabilities when talking to tootik. -`nobody` advertises support for RFC9421 and Ed25519 using [FEP-844e](https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md), to encourage other servers to use these capabilities when talking to tootik. +Before v0.21.0, tootik used to call this actor `nobody` and set the `sharedInbox` of non-portable actors to `nobody`'s inbox, to reduce the number of requests from servers that deduplicate outgoing requests by `sharedInbox` during wide delivery of posts. ## Forwarding diff --git a/cfg/cfg.go b/cfg/cfg.go index fbe93a5b..f7cea570 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -165,7 +165,7 @@ func (c *Config) FillDefaults() { c.CompiledUserNameRegex = regexp.MustCompile(c.UserNameRegex) if c.ForbiddenUserNameRegex == "" { - c.ForbiddenUserNameRegex = `^(root|localhost|ip6-.*|.*(admin|tootik).*)$` + c.ForbiddenUserNameRegex = `^(actor|nobody|root|localhost|ip6-.*|.*(admin|tootik).*)$` } c.CompiledForbiddenUserNameRegex = regexp.MustCompile(c.ForbiddenUserNameRegex) diff --git a/cluster/followers_sync_test.go b/cluster/followers_sync_test.go index 5d688bd2..6759cfa9 100644 --- a/cluster/followers_sync_test.go +++ b/cluster/followers_sync_test.go @@ -68,7 +68,7 @@ func TestCluster_FollowersSyncMissingRemoteFollow(t *testing.T) { Config: cluster["a.localdomain"].Config, DB: cluster["a.localdomain"].DB, Resolver: cluster["a.localdomain"].Resolver, - Keys: cluster["a.localdomain"].NobodyKeys, + Keys: cluster["a.localdomain"].AppActorKeys, Inbox: cluster["a.localdomain"].Inbox, } if _, err := syncer.ProcessBatch(t.Context()); err != nil { @@ -128,7 +128,7 @@ func TestCluster_FollowersSyncMissingLocalFollow(t *testing.T) { Config: cluster["a.localdomain"].Config, DB: cluster["a.localdomain"].DB, Resolver: cluster["a.localdomain"].Resolver, - Keys: cluster["a.localdomain"].NobodyKeys, + Keys: cluster["a.localdomain"].AppActorKeys, Inbox: cluster["a.localdomain"].Inbox, } if _, err := syncer.ProcessBatch(t.Context()); err != nil { @@ -198,7 +198,7 @@ func TestCluster_FollowersSyncMissingRemoteFollowPortableActor(t *testing.T) { Config: cluster["a.localdomain"].Config, DB: cluster["a.localdomain"].DB, Resolver: cluster["a.localdomain"].Resolver, - Keys: cluster["a.localdomain"].NobodyKeys, + Keys: cluster["a.localdomain"].AppActorKeys, Inbox: cluster["a.localdomain"].Inbox, } if _, err := syncer.ProcessBatch(t.Context()); err != nil { @@ -268,7 +268,7 @@ func TestCluster_FollowersSyncMissingLocalFollowPortableActor(t *testing.T) { Config: cluster["a.localdomain"].Config, DB: cluster["a.localdomain"].DB, Resolver: cluster["a.localdomain"].Resolver, - Keys: cluster["a.localdomain"].NobodyKeys, + Keys: cluster["a.localdomain"].AppActorKeys, Inbox: cluster["a.localdomain"].Inbox, } if _, err := syncer.ProcessBatch(t.Context()); err != nil { diff --git a/cluster/move_test.go b/cluster/move_test.go index acd9fa23..fc14360e 100644 --- a/cluster/move_test.go +++ b/cluster/move_test.go @@ -57,7 +57,7 @@ func TestCluster_MovedAccount(t *testing.T) { Domain: "b.localdomain", DB: cluster["b.localdomain"].DB, Resolver: cluster["b.localdomain"].Resolver, - Keys: cluster["b.localdomain"].NobodyKeys, + Keys: cluster["b.localdomain"].AppActorKeys, Inbox: cluster["b.localdomain"].Inbox, } if err := mover.Run(t.Context()); err != nil { diff --git a/cluster/server.go b/cluster/server.go index 16f67bd8..67492dc1 100644 --- a/cluster/server.go +++ b/cluster/server.go @@ -44,17 +44,17 @@ import ( ) type Server struct { - Test *testing.T - Domain string - Config *cfg.Config - DB *sql.DB - Resolver *fed.Resolver - NobodyKeys [2]httpsig.Key - Frontend gemini.Listener - Backend http.Handler - Inbox *inbox.Inbox - Incoming *inbox.Queue - Outgoing *fed.Queue + Test *testing.T + Domain string + Config *cfg.Config + DB *sql.DB + Resolver *fed.Resolver + AppActorKeys [2]httpsig.Key + Frontend gemini.Listener + Backend http.Handler + Inbox *inbox.Inbox + Incoming *inbox.Queue + Outgoing *fed.Queue listener, tlsListener net.Listener socketPath string @@ -160,9 +160,9 @@ func NewServer(t *testing.T, domain string, client fed.Client) *Server { resolver := fed.NewResolver(nil, domain, &cfg, client, db) - _, nobodyKeys, err := user.CreateNobody(t.Context(), domain, db, &cfg) + appActor, appActorKeys, err := user.CreateApplicationActor(t.Context(), domain, db, &cfg) if err != nil { - t.Fatalf("Failed to run create the nobody user: %v", err) + t.Fatalf("Failed to create the application actor: %v", err) } localInbox := &inbox.Inbox{ @@ -182,11 +182,12 @@ func NewServer(t *testing.T, domain string, client fed.Client) *Server { } backend, err := (&fed.Listener{ - Domain: domain, - Config: &cfg, - DB: db, - ActorKeys: nobodyKeys, - Resolver: resolver, + Domain: domain, + Config: &cfg, + DB: db, + AppActor: appActor, + AppActorKeys: appActorKeys, + Resolver: resolver, }).NewHandler() if err != nil { t.Fatalf("Failed to run create the federation handler: %v", err) @@ -208,12 +209,12 @@ func NewServer(t *testing.T, domain string, client fed.Client) *Server { tlsListener := tls.NewListener(listener, &serverCfg) return &Server{ - Test: t, - Domain: domain, - Config: &cfg, - DB: db, - Resolver: resolver, - NobodyKeys: nobodyKeys, + Test: t, + Domain: domain, + Config: &cfg, + DB: db, + Resolver: resolver, + AppActorKeys: appActorKeys, Frontend: gemini.Listener{ Domain: domain, Config: &cfg, @@ -227,7 +228,7 @@ func NewServer(t *testing.T, domain string, client fed.Client) *Server { DB: db, Inbox: localInbox, Resolver: resolver, - Keys: nobodyKeys, + Keys: appActorKeys, }, Outgoing: &fed.Queue{ Domain: domain, diff --git a/cmd/tootik/main.go b/cmd/tootik/main.go index 6718e5d0..393460f2 100644 --- a/cmd/tootik/main.go +++ b/cmd/tootik/main.go @@ -193,7 +193,7 @@ func main() { panic(err) } - _, nobodyKeys, err := user.CreateNobody(ctx, *domain, db, &cfg) + appActor, appActorKeys, err := user.CreateApplicationActor(ctx, *domain, db, &cfg) if err != nil { panic(err) } @@ -316,16 +316,17 @@ func main() { { "HTTPS", &fed.Listener{ - Domain: *domain, - Config: &cfg, - DB: db, - ActorKeys: nobodyKeys, - Resolver: resolver, - Addr: *addr, - Cert: *cert, - Key: *key, - Plain: *plain, - BlockList: blockList, + Domain: *domain, + Config: &cfg, + DB: db, + AppActor: appActor, + AppActorKeys: appActorKeys, + Resolver: resolver, + Addr: *addr, + Cert: *cert, + Key: *key, + Plain: *plain, + BlockList: blockList, }, }, { @@ -362,7 +363,7 @@ func main() { DB: db, Inbox: localInbox, Resolver: resolver, - Keys: nobodyKeys, + Keys: appActorKeys, }, }, { @@ -416,7 +417,7 @@ func main() { DB: db, Inbox: localInbox, Resolver: resolver, - Keys: nobodyKeys, + Keys: appActorKeys, }, }, { @@ -427,7 +428,7 @@ func main() { Config: &cfg, DB: db, Resolver: resolver, - Keys: nobodyKeys, + Keys: appActorKeys, Inbox: localInbox, }, }, diff --git a/fed/apgateway.go b/fed/apgateway.go index 51c95f51..a91cf4eb 100644 --- a/fed/apgateway.go +++ b/fed/apgateway.go @@ -79,7 +79,7 @@ func (l *Listener) handleAPGatewayPost(w http.ResponseWriter, r *http.Request) { } func (l *Listener) handleApGatewayFollowers(w http.ResponseWriter, r *http.Request, did string) { - _, sender, err := l.verifyRequest(r, nil, ap.InstanceActor, l.ActorKeys) + _, sender, err := l.verifyRequest(r, nil, ap.InstanceActor, l.AppActorKeys) if err != nil { slog.Warn("Failed to verify followers request", "error", err) w.WriteHeader(http.StatusUnauthorized) diff --git a/fed/followers.go b/fed/followers.go index d9f1cd2d..7653ee28 100644 --- a/fed/followers.go +++ b/fed/followers.go @@ -129,7 +129,7 @@ func (f partialFollowers) Digest(ctx context.Context, db *sql.DB, domain string, func (l *Listener) handleFollowers(w http.ResponseWriter, r *http.Request) { name := r.PathValue("username") - _, sender, err := l.verifyRequest(r, nil, ap.InstanceActor, l.ActorKeys) + _, sender, err := l.verifyRequest(r, nil, ap.InstanceActor, l.AppActorKeys) if err != nil { slog.Warn("Failed to verify followers request", "error", err) w.WriteHeader(http.StatusUnauthorized) diff --git a/fed/inbox.go b/fed/inbox.go index 89adccd0..d9f3c609 100644 --- a/fed/inbox.go +++ b/fed/inbox.go @@ -284,7 +284,7 @@ func (l *Listener) fetchObject(ctx context.Context, id string, keys [2]httpsig.K } func (l *Listener) handleSharedInbox(w http.ResponseWriter, r *http.Request) { - l.doHandleInbox(w, r, l.ActorKeys) + l.doHandleInbox(w, r, l.AppActorKeys) } func (l *Listener) handleInbox(w http.ResponseWriter, r *http.Request) { diff --git a/fed/index.go b/fed/index.go index 93a66d7a..8f198bf3 100644 --- a/fed/index.go +++ b/fed/index.go @@ -21,7 +21,7 @@ import "net/http" func (l *Listener) handleIndex(w http.ResponseWriter, r *http.Request) { // this is how PieFed fetches the instance actor to detect its inbox and use it as a shared inbox for this instance if accept := r.Header.Get("Accept"); accept == "application/activity+json" || accept == `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` { - l.doHandleUser(w, r, "nobody") + l.doHandleUser(w, r, "actor") return } diff --git a/fed/listener.go b/fed/listener.go index d09d6ee5..cbed3caf 100644 --- a/fed/listener.go +++ b/fed/listener.go @@ -29,22 +29,24 @@ import ( "sync" "time" + "github.com/dimkr/tootik/ap" "github.com/dimkr/tootik/cfg" "github.com/dimkr/tootik/httpsig" "github.com/fsnotify/fsnotify" ) type Listener struct { - Domain string - Config *cfg.Config - DB *sql.DB - Resolver *Resolver - ActorKeys [2]httpsig.Key - Addr string - Cert string - Key string - Plain bool - BlockList *BlockList + Domain string + Config *cfg.Config + DB *sql.DB + Resolver *Resolver + AppActor *ap.Actor + AppActorKeys [2]httpsig.Key + Addr string + Cert string + Key string + Plain bool + BlockList *BlockList } const certReloadDelay = time.Second * 5 @@ -59,8 +61,8 @@ func (l *Listener) newHandler() (*http.ServeMux, error) { mux.HandleFunc("GET /actor", l.handleActor) mux.HandleFunc("GET /icon/{username}", l.handleIcon) mux.HandleFunc("POST /inbox/{username}", l.handleInbox) - mux.HandleFunc("POST /inbox/nobody", l.handleSharedInbox) - mux.HandleFunc("POST /inbox", l.handleSharedInbox) // PieFed falls back https://$domain/inbox if it can't fetch instance actor + mux.HandleFunc("POST /inbox/actor", l.handleSharedInbox) + mux.HandleFunc("POST /inbox", l.handleSharedInbox) mux.HandleFunc("GET /outbox/{username}", l.handleOutbox) mux.HandleFunc("GET /post/{hash}", l.handlePost) mux.HandleFunc("GET /create/{hash}", l.handleCreate) @@ -73,7 +75,7 @@ func (l *Listener) newHandler() (*http.ServeMux, error) { w.WriteHeader(http.StatusNotFound) }) - if err := addNodeInfo(mux, l.Domain, l.Config, l.DB); err != nil { + if err := l.addNodeInfo(mux); err != nil { return nil, err } diff --git a/fed/nodeinfo.go b/fed/nodeinfo.go index e1efb301..0c8643e2 100644 --- a/fed/nodeinfo.go +++ b/fed/nodeinfo.go @@ -17,7 +17,6 @@ limitations under the License. package fed import ( - "database/sql" "encoding/json" "fmt" "log/slog" @@ -25,13 +24,12 @@ import ( "time" "github.com/dimkr/tootik/buildinfo" - "github.com/dimkr/tootik/cfg" "github.com/dimkr/tootik/lock" ) const nodeInfoUpdateInterval = time.Hour * 6 -func addNodeInfo20Stub(mux *http.ServeMux, cfg *cfg.Config) error { +func (l *Listener) addNodeInfo20Stub(mux *http.ServeMux) error { body, err := json.Marshal(map[string]any{ "version": "2.0", "software": map[string]any{ @@ -53,7 +51,7 @@ func addNodeInfo20Stub(mux *http.ServeMux, cfg *cfg.Config) error { }, "localPosts": 0, }, - "openRegistrations": !cfg.RequireInvitation, + "openRegistrations": !l.Config.RequireInvitation, "metadata": map[string]any{}, }) if err != nil { @@ -68,16 +66,16 @@ func addNodeInfo20Stub(mux *http.ServeMux, cfg *cfg.Config) error { return nil } -func addNodeInfo(mux *http.ServeMux, domain string, cfg *cfg.Config, db *sql.DB) error { +func (l *Listener) addNodeInfo(mux *http.ServeMux) error { if body, err := json.Marshal(map[string]any{ "links": []map[string]any{ { "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", - "href": fmt.Sprintf("https://%s/nodeinfo/2.0", domain), + "href": fmt.Sprintf("https://%s/nodeinfo/2.0", l.Domain), }, { "rel": "https://www.w3.org/ns/activitystreams#Application", - "href": fmt.Sprintf("https://%s/user/nobody", domain), + "href": l.AppActor.ID, }, }, }); err != nil { @@ -89,25 +87,25 @@ func addNodeInfo(mux *http.ServeMux, domain string, cfg *cfg.Config, db *sql.DB) }) } - if !cfg.FillNodeInfoUsage { - return addNodeInfo20Stub(mux, cfg) + if !l.Config.FillNodeInfoUsage { + return l.addNodeInfo20Stub(mux) } - l := lock.New() + cacheLock := lock.New() var total, activeHalfYear, activeMonth, localPosts int64 var last time.Time mux.HandleFunc("GET /nodeinfo/2.0", func(w http.ResponseWriter, r *http.Request) { - if err := l.Lock(r.Context()); err != nil { + if err := cacheLock.Lock(r.Context()); err != nil { slog.Warn("Failed to build nodeinfo response", "error", err) w.WriteHeader(http.StatusInternalServerError) return } - defer l.Unlock() + defer cacheLock.Unlock() now := time.Now() if now.Sub(last) >= nodeInfoUpdateInterval { - if err := db.QueryRowContext( + if err := l.DB.QueryRowContext( r.Context(), ` select @@ -130,7 +128,7 @@ func addNodeInfo(mux *http.ServeMux, domain string, cfg *cfg.Config, db *sql.DB) ), (select count(*) from notes where host = $1) `, - domain, + l.Domain, ).Scan(&total, &activeHalfYear, &activeMonth, &localPosts); err != nil { slog.Warn("Failed to build nodeinfo response", "error", err) w.WriteHeader(http.StatusInternalServerError) @@ -161,7 +159,7 @@ func addNodeInfo(mux *http.ServeMux, domain string, cfg *cfg.Config, db *sql.DB) }, "localPosts": localPosts, }, - "openRegistrations": !cfg.RequireInvitation, + "openRegistrations": !l.Config.RequireInvitation, "metadata": map[string]any{}, }); err != nil { slog.Warn("Failed to build nodeinfo response", "error", err) diff --git a/fed/resolve_test.go b/fed/resolve_test.go index 75ccbf2e..4a41a6fa 100644 --- a/fed/resolve_test.go +++ b/fed/resolve_test.go @@ -94,17 +94,17 @@ func TestResolve_LocalActor(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - nobody, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + app, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) - actor, err := resolver.ResolveID(context.Background(), key, nobody.ID, 0) + actor, err := resolver.ResolveID(context.Background(), key, app.ID, 0) assert.NoError(err) assert.Empty(client.Data) - assert.Equal(nobody.ID, actor.ID) - assert.Equal(nobody.Inbox, actor.Inbox) + assert.Equal(app.ID, actor.ID) + assert.Equal(app.Inbox, actor.Inbox) } func TestResolve_LocalActorDoesNotExist(t *testing.T) { @@ -130,7 +130,7 @@ func TestResolve_LocalActorDoesNotExist(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -211,7 +211,7 @@ func TestResolve_FederatedInstanceActor(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -247,7 +247,7 @@ func TestResolve_FederatedActorInvalidURL(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -279,7 +279,7 @@ func TestResolve_FederatedActorInvalidScheme(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -360,7 +360,7 @@ func TestResolve_FederatedActorFirstTime(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -396,7 +396,7 @@ func TestResolve_FederatedActorFirstTimeOffline(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -429,7 +429,7 @@ func TestResolve_FederatedActorFirstTimeCancelled(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -523,7 +523,7 @@ func TestResolve_FederatedActorFirstTimeInvalidWebFingerLink(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -608,7 +608,7 @@ func TestResolve_FederatedActorFirstTimeActorIDMismatch(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -690,7 +690,7 @@ func TestResolve_FederatedActorCached(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -756,7 +756,7 @@ func TestResolve_FederatedActorCachedInvalidActorHost(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -897,7 +897,7 @@ func TestResolve_FederatedActorCachedActorHostSubdomain(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1043,7 +1043,7 @@ func TestResolve_FederatedActorCachedActorHostSubdomainFetchedRecently(t *testin assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1137,7 +1137,7 @@ func TestResolve_FederatedActorCachedActorIDChanged(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1258,7 +1258,7 @@ func TestResolve_FederatedActorCachedButBlocked(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1356,7 +1356,7 @@ func TestResolve_FederatedActorOldCache(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1509,7 +1509,7 @@ func TestResolve_FederatedActorOldCacheWasSuspended(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1659,7 +1659,7 @@ func TestResolve_FederatedActorOldCacheWasNew(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1808,7 +1808,7 @@ func TestResolve_FederatedActorOldCacheUpdateFailed(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -1934,7 +1934,7 @@ func TestResolve_FederatedActorOldCacheStillNew(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2074,7 +2074,7 @@ func TestResolve_FederatedActorOldCacheWasOld(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2218,7 +2218,7 @@ func TestResolve_FederatedActorOldCacheWasNewNowUnknown(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2358,7 +2358,7 @@ func TestResolve_FederatedActorOldCacheFetchedRecently(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2452,7 +2452,7 @@ func TestResolve_FederatedActorOldCacheButOffline(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2546,7 +2546,7 @@ func TestResolve_FederatedActorOldCacheExpiredDomain(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2614,7 +2614,7 @@ func TestResolve_FederatedActorOldCacheInvalidID(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2696,7 +2696,7 @@ func TestResolve_FederatedActorOldCacheInvalidWebFingerResponse(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2801,7 +2801,7 @@ func TestResolve_FederatedActorOldCacheBigWebFingerResponse(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -2924,7 +2924,7 @@ func TestResolve_FederatedActorOldCacheInvalidActor(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -3051,7 +3051,7 @@ func TestResolve_FederatedActorOldCacheBigActor(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -3170,7 +3170,7 @@ func TestResolve_FederatedActorFirstTimeThroughKey(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -3299,7 +3299,7 @@ func TestResolve_FederatedActorNoProfileLink(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -3426,7 +3426,7 @@ func TestResolve_FederatedActorOldCacheWebFingerError(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -3533,7 +3533,7 @@ func TestResolve_FederatedActorOldCacheActorError(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -3663,7 +3663,7 @@ func TestResolve_FederatedActorOldCacheActorDeleted(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) tx, err := db.BeginTx(context.Background(), nil) @@ -3804,7 +3804,7 @@ func TestResolve_FederatedActorFirstTimeWrongID(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -3864,7 +3864,7 @@ func TestResolve_FederatedActorFirstTimeDeleted(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) tx, err := db.BeginTx(context.Background(), nil) @@ -3968,7 +3968,7 @@ func TestResolve_FederatedActorFirstTimeTooYoung(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -4051,7 +4051,7 @@ func TestResolve_FederatedActorFirstTimeSuspended(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -4133,7 +4133,7 @@ func TestResolve_FederatedActorWrongIDCached(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -4224,7 +4224,7 @@ func TestResolve_FederatedActorWrongIDCachedOldCache(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) @@ -4370,7 +4370,7 @@ func TestResolve_FederatedActorWrongIDOldCache(t *testing.T) { assert.NoError(migrations.Run(context.Background(), "localhost.localdomain", db)) - _, key, err := user.CreateNobody(context.Background(), "localhost.localdomain", db, &cfg) + _, key, err := user.CreateApplicationActor(context.Background(), "localhost.localdomain", db, &cfg) assert.NoError(err) resolver := NewResolver(&blockList, "localhost.localdomain", &cfg, &client, db) diff --git a/fed/user.go b/fed/user.go index bb34cdf9..7ef0ac2b 100644 --- a/fed/user.go +++ b/fed/user.go @@ -58,5 +58,5 @@ func (l *Listener) handleUser(w http.ResponseWriter, r *http.Request) { } func (l *Listener) handleActor(w http.ResponseWriter, r *http.Request) { - l.doHandleUser(w, r, "nobody") + l.doHandleUser(w, r, "actor") } diff --git a/fed/webfinger.go b/fed/webfinger.go index 3fa70eb6..25a1dc84 100644 --- a/fed/webfinger.go +++ b/fed/webfinger.go @@ -87,9 +87,9 @@ func (l *Listener) handleWebFinger(w http.ResponseWriter, r *http.Request) { username = fields[0] } - // nobody is our equivalent of the Mastodon "instance actor" + // actor is our equivalent of the Mastodon "instance actor" if username == l.Domain { - username = "nobody" + username = "actor" } slog.Info("Looking up resource", "resource", resource, "user", username) diff --git a/front/register.go b/front/register.go index 9fa3a84c..7fbb5217 100644 --- a/front/register.go +++ b/front/register.go @@ -123,14 +123,7 @@ func (h *Handler) register(w text.Writer, r *Request, args ...string) { return } - pub, priv, err := ed25519.GenerateKey(nil) - if err != nil { - r.Log.Warn("Failed to generate key", "error", err) - w.Status(40, "Failed to generate key") - return - } - - if _, _, err := user.CreatePortable(r.Context, h.Domain, h.DB, h.Config, userName, clientCert, priv, pub); err != nil { + if _, _, err := user.CreatePortable(r.Context, h.Domain, h.DB, h.Config, userName, ap.Person, clientCert); err != nil { r.Log.Warn("Failed to create new portable user", "name", userName, "error", err) w.Status(40, "Failed to create new user") return @@ -149,7 +142,7 @@ func (h *Handler) register(w text.Writer, r *Request, args ...string) { return } - if _, _, err := user.CreatePortable(r.Context, h.Domain, h.DB, h.Config, userName, clientCert, key, key.Public().(ed25519.PublicKey)); err != nil { + if _, _, err := user.CreatePortableWithKey(r.Context, h.Domain, h.DB, h.Config, userName, ap.Person, clientCert, key, key.Public().(ed25519.PublicKey)); err != nil { r.Log.Warn("Failed to create new portable user", "name", userName, "error", err) w.Status(40, "Failed to create new user") return diff --git a/front/user/app.go b/front/user/app.go new file mode 100644 index 00000000..2d26ce60 --- /dev/null +++ b/front/user/app.go @@ -0,0 +1,60 @@ +/* +Copyright 2023 - 2025 Dima Krasner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package user + +import ( + "context" + "crypto/ed25519" + "crypto/x509" + "database/sql" + "errors" + "fmt" + + "github.com/dimkr/tootik/ap" + "github.com/dimkr/tootik/cfg" + "github.com/dimkr/tootik/httpsig" +) + +// CreateApplicationActor creates the special "actor" user. +// This user is used to sign outgoing requests not initiated by a particular user. +func CreateApplicationActor(ctx context.Context, domain string, db *sql.DB, cfg *cfg.Config) (*ap.Actor, [2]httpsig.Key, error) { + var actor ap.Actor + var rsaPrivKeyDer, ed25519PrivKey []byte + if err := db.QueryRowContext( + ctx, + `select json(actor), rsaprivkey, ed25519privkey from persons where actor->>'$.preferredUsername' = 'actor' and host = ?`, + domain, + ).Scan( + &actor, + &rsaPrivKeyDer, + &ed25519PrivKey, + ); errors.Is(err, sql.ErrNoRows) { + return CreatePortable(ctx, domain, db, cfg, "actor", ap.Application, nil) + } else if err != nil { + return nil, [2]httpsig.Key{}, fmt.Errorf("failed to fetch application actor: %w", err) + } + + rsaPrivKey, err := x509.ParsePKCS1PrivateKey(rsaPrivKeyDer) + if err != nil { + return nil, [2]httpsig.Key{}, err + } + + return &actor, [2]httpsig.Key{ + {ID: actor.PublicKey.ID, PrivateKey: rsaPrivKey}, + {ID: actor.AssertionMethod[0].ID, PrivateKey: ed25519.NewKeyFromSeed(ed25519PrivKey)}, + }, err +} diff --git a/front/user/create.go b/front/user/create.go index ec4dddf4..f4709d78 100644 --- a/front/user/create.go +++ b/front/user/create.go @@ -139,6 +139,35 @@ func CreatePortable( db *sql.DB, cfg *cfg.Config, name string, + actorType ap.ActorType, + cert *x509.Certificate, +) (*ap.Actor, [2]httpsig.Key, error) { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + return nil, [2]httpsig.Key{}, fmt.Errorf("failed to generate Ed25519 key for %s: %w", name, err) + } + + return CreatePortableWithKey( + ctx, + domain, + db, + cfg, + name, + actorType, + cert, + priv, + pub, + ) +} + +// CreatePortableWithKey creates a new portable user using a given key. +func CreatePortableWithKey( + ctx context.Context, + domain string, + db *sql.DB, + cfg *cfg.Config, + name string, + actorType ap.ActorType, cert *x509.Certificate, ed25519Priv ed25519.PrivateKey, ed25519Pub ed25519.PublicKey, @@ -158,7 +187,7 @@ func CreatePortable( "https://w3id.org/security/v1", }, ID: id, - Type: ap.Person, + Type: actorType, PreferredUsername: name, Icon: []ap.Attachment{ { @@ -186,6 +215,20 @@ func CreatePortable( }, } + if actorType == ap.Application { + actor.Generator.Type = ap.Application + actor.Generator.Implements = []ap.Implement{ + { + Name: "RFC-9421: HTTP Message Signatures", + Href: "https://datatracker.ietf.org/doc/html/rfc9421", + }, + { + Name: "RFC-9421 signatures using the Ed25519 algorithm", + Href: "https://datatracker.ietf.org/doc/html/rfc9421#name-eddsa-using-curve-edwards25", + }, + } + } + keys := [2]httpsig.Key{ {ID: actor.PublicKey.ID, PrivateKey: rsaPriv}, {ID: actor.AssertionMethod[0].ID, PrivateKey: ed25519Priv}, @@ -229,9 +272,8 @@ func Create(ctx context.Context, domain string, db *sql.DB, cfg *cfg.Config, nam }, Inbox: fmt.Sprintf("https://%s/inbox/%s", domain, name), Outbox: fmt.Sprintf("https://%s/outbox/%s", domain, name), - // use nobody's inbox as a shared inbox Endpoints: map[string]string{ - "sharedInbox": fmt.Sprintf("https://%s/inbox/nobody", domain), + "sharedInbox": fmt.Sprintf("https://%s/inbox", domain), }, Followers: fmt.Sprintf("https://%s/followers/%s", domain, name), PublicKey: ap.PublicKey{ diff --git a/front/user/nobody.go b/front/user/nobody.go deleted file mode 100644 index 1ee7d004..00000000 --- a/front/user/nobody.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright 2023 - 2025 Dima Krasner - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package user - -import ( - "context" - "crypto/ed25519" - "crypto/x509" - "database/sql" - "errors" - "fmt" - - "github.com/dimkr/tootik/ap" - "github.com/dimkr/tootik/cfg" - "github.com/dimkr/tootik/httpsig" -) - -// CreateNobody creates the special "nobdoy" user. -// This user is used to sign outgoing requests not initiated by a particular user. -func CreateNobody(ctx context.Context, domain string, db *sql.DB, cfg *cfg.Config) (*ap.Actor, [2]httpsig.Key, error) { - var actor ap.Actor - var rsaPrivKeyDer, ed25519PrivKey []byte - if err := db.QueryRowContext(ctx, `select json(actor), rsaprivkey, ed25519privkey from persons where actor->>'$.preferredUsername' = 'nobody' and host = ?`, domain).Scan(&actor, &rsaPrivKeyDer, &ed25519PrivKey); err != nil && !errors.Is(err, sql.ErrNoRows) { - return nil, [2]httpsig.Key{}, fmt.Errorf("failed to create nobody user: %w", err) - } else if err == nil { - rsaPrivKey, err := x509.ParsePKCS1PrivateKey(rsaPrivKeyDer) - if err != nil { - return nil, [2]httpsig.Key{}, err - } - - return &actor, [2]httpsig.Key{ - {ID: actor.PublicKey.ID, PrivateKey: rsaPrivKey}, - {ID: actor.AssertionMethod[0].ID, PrivateKey: ed25519.NewKeyFromSeed(ed25519PrivKey)}, - }, err - } - - return Create(ctx, domain, db, cfg, "nobody", ap.Application, nil) -} diff --git a/migrations/056_actor.go b/migrations/056_actor.go new file mode 100644 index 00000000..d9db28e0 --- /dev/null +++ b/migrations/056_actor.go @@ -0,0 +1,47 @@ +package migrations + +import ( + "context" + "crypto/ed25519" + "database/sql" + "fmt" + + "github.com/dimkr/tootik/ap" + "github.com/dimkr/tootik/httpsig" + "github.com/dimkr/tootik/proof" +) + +func actor(ctx context.Context, domain string, tx *sql.Tx) error { + if rows, err := tx.QueryContext(ctx, `SELECT JSON(actor), ed25519privkey FROM persons WHERE ed25519privkey IS NOT NULL AND actor->>'$.endpoints.sharedInbox' IS NOT NULL`); err != nil { + return err + } else { + defer rows.Close() + + sharedInbox := fmt.Sprintf("https://%s/inbox", domain) + + for rows.Next() { + var actor ap.Actor + var ed25519PrivKey []byte + if err := rows.Scan(&actor, &ed25519PrivKey); err != nil { + return err + } + + actor.Endpoints["sharedInbox"] = sharedInbox + + actor.Proof, err = proof.Create(httpsig.Key{ID: actor.AssertionMethod[0].ID, PrivateKey: ed25519.NewKeyFromSeed(ed25519PrivKey)}, &actor) + if err != nil { + return err + } + + if _, err := tx.ExecContext(ctx, `UPDATE persons SET actor = JSONB(?) WHERE id = ?`, &actor, actor.ID); err != nil { + return err + } + } + + if err := rows.Err(); err != nil { + return err + } + } + + return nil +} diff --git a/test/move_test.go b/test/move_test.go index ca183d30..698b03c3 100644 --- a/test/move_test.go +++ b/test/move_test.go @@ -67,7 +67,7 @@ func TestMove_FederatedToFederated(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -115,7 +115,7 @@ func TestMove_FederatedToFederatedTwoAccounts(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -163,7 +163,7 @@ func TestMove_FederatedToFederatedNotLinked(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -204,7 +204,7 @@ func TestMove_FederatedToLocal(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -248,7 +248,7 @@ func TestMove_FederatedToLocalLinked(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -305,7 +305,7 @@ func TestMove_FollowingBoth(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -359,7 +359,7 @@ func TestMove_LocalToLocal(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -394,7 +394,7 @@ func TestMove_LocalToLocalNoFollowers(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -442,7 +442,7 @@ func TestMove_LocalToFederated(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) @@ -551,7 +551,7 @@ func TestMove_LocalToFederatedAlreadyMoved(t *testing.T) { Domain: domain, DB: server.db, Resolver: resolver, - Keys: server.NobodyKeys, + Keys: server.AppActorKeys, Inbox: server.inbox, } assert.NoError(mover.Run(context.Background())) diff --git a/test/server.go b/test/server.go index 2e21601b..4b8cc431 100644 --- a/test/server.go +++ b/test/server.go @@ -40,16 +40,16 @@ import ( const domain = "localhost.localdomain:8443" type server struct { - cfg *cfg.Config - db *sql.DB - dbPath string - handler front.Handler - inbox *inbox.Inbox - queue *inbox.Queue - Alice *ap.Actor - Bob *ap.Actor - Carol *ap.Actor - NobodyKeys [2]httpsig.Key + cfg *cfg.Config + db *sql.DB + dbPath string + handler front.Handler + inbox *inbox.Inbox + queue *inbox.Queue + Alice *ap.Actor + Bob *ap.Actor + Carol *ap.Actor + AppActorKeys [2]httpsig.Key } func (s *server) Shutdown() { @@ -94,7 +94,7 @@ func newTestServer() *server { panic(err) } - _, nobodyKeys, err := user.CreateNobody(context.Background(), domain, db, &cfg) + _, appActorKeys, err := user.CreateApplicationActor(context.Background(), domain, db, &cfg) if err != nil { panic(err) } @@ -112,7 +112,7 @@ func newTestServer() *server { DB: db, Inbox: localInbox, Resolver: resolver, - Keys: nobodyKeys, + Keys: appActorKeys, } handler, err := front.NewHandler(domain, &cfg, resolver, db, localInbox) @@ -121,16 +121,16 @@ func newTestServer() *server { } return &server{ - cfg: &cfg, - dbPath: path, - db: db, - handler: handler, - inbox: localInbox, - queue: queue, - Alice: alice, - Bob: bob, - Carol: carol, - NobodyKeys: nobodyKeys, + cfg: &cfg, + dbPath: path, + db: db, + handler: handler, + inbox: localInbox, + queue: queue, + Alice: alice, + Bob: bob, + Carol: carol, + AppActorKeys: appActorKeys, } }