diff --git a/FEDERATION.md b/FEDERATION.md index b6e923ac..b61d2eef 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 @@ -149,14 +149,14 @@ Support for data portability comes into play in 5 main areas: ## Registration +Since v0.21.0, tootik no longer offers choice between 'traditional' and portable actors: all newly registered users are portable actors. + A portable actor is created by generating or supplying a pre-generated, base58-encoded Ed25519 private key during registration. The key, like the user's `preferredUsername`, must be unique per tootik instance. No matter if the key was generated by tootik or provided by the user, the user can recover it through the settings page. tootik does not support the [FEP-ae97](https://codeberg.org/fediverse/fep/src/branch/main/fep/ae97/fep-ae97.md) registration flow. -Registration of portable actors is disabled by default (see `EnablePortableActorRegistration`). - ## Discovery Portable actors can be looked up normally, over [WebFinger](https://www.rfc-editor.org/rfc/rfc7033): diff --git a/cfg/cfg.go b/cfg/cfg.go index fbe93a5b..4e587388 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -27,17 +27,17 @@ import ( type Config struct { DatabaseOptions string - RequireRegistration bool - RequireInvitation bool - MaxInvitationsPerUser *int - InvitationTimeout time.Duration - RegistrationInterval time.Duration - CertificateApprovalTimeout time.Duration - UserNameRegex string - CompiledUserNameRegex *regexp.Regexp `json:"-"` - ForbiddenUserNameRegex string - CompiledForbiddenUserNameRegex *regexp.Regexp `json:"-"` - EnablePortableActorRegistration bool + RequireRegistration bool + RequireInvitation bool + MaxInvitationsPerUser *int + InvitationTimeout time.Duration + RegistrationInterval time.Duration + CertificateApprovalTimeout time.Duration + UserNameRegex string + CompiledUserNameRegex *regexp.Regexp `json:"-"` + ForbiddenUserNameRegex string + CompiledForbiddenUserNameRegex *regexp.Regexp `json:"-"` + EnableNonPortableActorRegistration bool MaxPostsLength int MaxPostsPerDay int64 @@ -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/community_test.go b/cluster/community_test.go index 62219e63..c3131361 100644 --- a/cluster/community_test.go +++ b/cluster/community_test.go @@ -27,7 +27,7 @@ func TestCluster_PostInCommunity(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain", "g.localdomain") defer cluster.Stop() - if _, _, err := user.Create(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil { + if _, _, err := user.CreatePortable(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil { t.Fatal("Failed to create community") } @@ -93,7 +93,7 @@ func TestCluster_ReplyInCommunity(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain", "g.localdomain") defer cluster.Stop() - if _, _, err := user.Create(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil { + if _, _, err := user.CreatePortable(t.Context(), "g.localdomain", cluster["g.localdomain"].DB, cluster["g.localdomain"].Config, "stuff", ap.Group, nil); err != nil { t.Fatal("Failed to create community") } diff --git a/cluster/followers_sync_test.go b/cluster/followers_sync_test.go index 5d688bd2..21f04b42 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 { @@ -148,8 +148,6 @@ func TestCluster_FollowersSyncMissingRemoteFollowPortableActor(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain") defer cluster.Stop() - cluster["b.localdomain"].Config.EnablePortableActorRegistration = true - pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatalf("Failed to generate key: %v", err) @@ -198,7 +196,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 { @@ -218,8 +216,6 @@ func TestCluster_FollowersSyncMissingLocalFollowPortableActor(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain") defer cluster.Stop() - cluster["b.localdomain"].Config.EnablePortableActorRegistration = true - pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatalf("Failed to generate key: %v", err) @@ -268,7 +264,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/invitation_test.go b/cluster/invitation_test.go index 884cec13..ad7e6866 100644 --- a/cluster/invitation_test.go +++ b/cluster/invitation_test.go @@ -28,6 +28,7 @@ func TestServer_InvitationHappyFlow(t *testing.T) { alice := s.Register(aliceKeypair).OK() s.Config.RequireInvitation = true + s.Config.EnableNonPortableActorRegistration = true bobCode := "70bc9fdf-74a4-41e5-973d-08ba3fd23d74" carolCode := "ded3626c-ea4b-44cc-adf3-18510e7634e1" @@ -38,7 +39,9 @@ func TestServer_InvitationHappyFlow(t *testing.T) { FollowInput("➕ Generate", bobCode). Contains(Line{Type: Text, Text: "Code: " + bobCode}) - s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK() + accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(bobKeypair, accept.Path, "n").Follow("😈 My profile").OK() alice. Follow("⚙️ Settings"). @@ -47,7 +50,9 @@ func TestServer_InvitationHappyFlow(t *testing.T) { FollowInput("➕ Generate", carolCode). Contains(Line{Type: Text, Text: "Code: " + carolCode}) - s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode).Follow("😈 My profile").OK() + accept = s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(carolKeypair, accept.Path, "generate").Follow("😈 My profile").OK() } func TestServer_WrongCode(t *testing.T) { @@ -56,6 +61,7 @@ func TestServer_WrongCode(t *testing.T) { alice := s.Register(aliceKeypair).OK() s.Config.RequireInvitation = true + s.Config.EnableNonPortableActorRegistration = true bobCode := "70bc9fdf-74a4-41e5-973d-08ba3fd23d74" carolCode := "ded3626c-ea4b-44cc-adf3-18510e7634e1" @@ -68,7 +74,9 @@ func TestServer_WrongCode(t *testing.T) { s.HandleInput(bobKeypair, "/users/invitations/accept", carolCode).Error("40 Invalid invitation code") - s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK() + accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(bobKeypair, accept.Path, "generate").Follow("😈 My profile").OK() } func TestServer_ExpiredCode(t *testing.T) { @@ -92,7 +100,10 @@ func TestServer_ExpiredCode(t *testing.T) { s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Error("40 Invalid invitation code") s.Config.InvitationTimeout = time.Hour - s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK() + + accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(bobKeypair, accept.Path, "generate").Follow("😈 My profile").OK() case <-t.Context().Done(): t.Fail() @@ -114,7 +125,9 @@ func TestServer_CodeReuse(t *testing.T) { FollowInput("➕ Generate", bobCode). Contains(Line{Type: Text, Text: "Code: " + bobCode}) - s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK() + accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(bobKeypair, accept.Path, "generate").Follow("😈 My profile").OK() s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Error("40 Invalid invitation code") s.HandleInput(carolKeypair, "/users/invitations/accept", bobCode).Error("40 Invalid invitation code") @@ -128,6 +141,7 @@ func TestServer_InvitationLimit(t *testing.T) { s.Config.RequireInvitation = true limit := 1 s.Config.MaxInvitationsPerUser = &limit + s.Config.EnableNonPortableActorRegistration = true bobCode := "70bc9fdf-74a4-41e5-973d-08ba3fd23d74" carolCode := "ded3626c-ea4b-44cc-adf3-18510e7634e1" @@ -145,7 +159,9 @@ func TestServer_InvitationLimit(t *testing.T) { alice.Goto("/users/invitations/generate"). Error("40 Reached the maximum number of invitations") - s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode).Follow("😈 My profile").OK() + accept := s.HandleInput(bobKeypair, "/users/invitations/accept", bobCode) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(bobKeypair, accept.Path, "n").Follow("😈 My profile").OK() alice. Follow("⚙️ Settings"). @@ -156,7 +172,9 @@ func TestServer_InvitationLimit(t *testing.T) { NotContains(Line{Type: Link, Text: "➕ Generate", URL: "/users/invitations/generate"}). Contains(Line{Type: Text, Text: "Reached the maximum number of invitations."}) - s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode).Follow("😈 My profile").OK() + accept = s.HandleInput(carolKeypair, "/users/invitations/accept", carolCode) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(carolKeypair, accept.Path, "generate").Follow("😈 My profile").OK() limit = 3 alice. @@ -251,9 +269,9 @@ func TestServer_InvitationCreateAcceptDelete(t *testing.T) { t.Fatalf("Not found") } - s. - HandleInput(bobKeypair, "/users/invitations/accept", code). - OK() + accept := s.HandleInput(bobKeypair, "/users/invitations/accept", code) + accept.Error("11 base58-encoded Ed25519 private key or 'generate' to generate") + s.HandleInput(bobKeypair, accept.Path, "generate").OK() page. Follow("➖ Revoke"). 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/portability_test.go b/cluster/portability_test.go index 2f9d5fb0..45c3f31d 100644 --- a/cluster/portability_test.go +++ b/cluster/portability_test.go @@ -34,10 +34,6 @@ func TestCluster_ReplyForwardingPortableActors(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain") defer cluster.Stop() - cluster["a.localdomain"].Config.EnablePortableActorRegistration = true - cluster["b.localdomain"].Config.EnablePortableActorRegistration = true - cluster["c.localdomain"].Config.EnablePortableActorRegistration = true - alice := cluster["a.localdomain"].RegisterPortable(aliceKeypair).OK() bob := cluster["b.localdomain"].RegisterPortable(bobKeypair).OK() carol := cluster["c.localdomain"].RegisterPortable(carolKeypair).OK() @@ -103,10 +99,6 @@ func TestCluster_Gateways(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain") defer cluster.Stop() - cluster["a.localdomain"].Config.EnablePortableActorRegistration = true - cluster["b.localdomain"].Config.EnablePortableActorRegistration = true - cluster["c.localdomain"].Config.EnablePortableActorRegistration = true - pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatalf("Failed to generate key: %v", err) @@ -195,11 +187,9 @@ func TestCluster_ForwardedLegacyReply(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain") defer cluster.Stop() - cluster["a.localdomain"].Config.EnablePortableActorRegistration = true cluster["b.localdomain"].Config.RFC9421Threshold = 1 cluster["b.localdomain"].Config.Ed25519Threshold = 1 cluster["b.localdomain"].Config.DisableIntegrityProofs = true - cluster["c.localdomain"].Config.EnablePortableActorRegistration = true alice := cluster["a.localdomain"].RegisterPortable(aliceKeypair).OK() bob := cluster["b.localdomain"].Register(bobKeypair).OK() @@ -234,9 +224,6 @@ func TestCluster_ClientSideSigning(t *testing.T) { cluster := NewCluster(t, "a.localdomain", "b.localdomain", "c.localdomain") defer cluster.Stop() - cluster["a.localdomain"].Config.EnablePortableActorRegistration = true - cluster["c.localdomain"].Config.EnablePortableActorRegistration = true - pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatalf("Failed to generate key: %v", err) diff --git a/cluster/server.go b/cluster/server.go index 16f67bd8..387ba02f 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 @@ -150,6 +150,7 @@ func NewServer(t *testing.T, domain string, client fed.Client) *Server { cfg.FollowersSyncInterval = 0 cfg.Ed25519Threshold = 0.25 cfg.RFC9421Threshold = 0.5 + cfg.EnableNonPortableActorRegistration = true dbPath := filepath.Join(t.TempDir(), domain+".sqlite3") @@ -160,9 +161,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 +183,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 +210,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 +229,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..e1a33b54 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) } @@ -206,7 +206,7 @@ func main() { switch cmd { case "add-community": - _, _, err := user.Create(ctx, *domain, db, &cfg, flag.Arg(1), ap.Group, nil) + _, _, err := user.CreatePortable(ctx, *domain, db, &cfg, flag.Arg(1), ap.Group, nil) 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..fb4d529f 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 @@ -58,9 +60,9 @@ func (l *Listener) newHandler() (*http.ServeMux, error) { mux.HandleFunc("GET /user/{username}", l.handleUser) 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/{username}", l.handleInbox) // for compatibility with actors created by tootik<0.21.0 + 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..1d74c75b 100644 --- a/front/register.go +++ b/front/register.go @@ -93,55 +93,29 @@ func (h *Handler) register(w text.Writer, r *Request, args ...string) { switch r.URL.RawQuery { case "": - if h.Config.EnablePortableActorRegistration { - w.Status(10, "Create portable user? (y/n)") - return - } else if _, _, err := user.Create(r.Context, h.Domain, h.DB, h.Config, userName, ap.Person, clientCert); err != nil { - r.Log.Warn("Failed to create new user", "name", userName, "error", err) - w.Status(40, "Failed to create new user") + w.Status(11, "base58-encoded Ed25519 private key or 'generate' to generate") + return + + case "n": + if !h.Config.EnableNonPortableActorRegistration { + w.Status(40, "Registration of non-portable actors is disabled") return } - case "n": if _, _, err := user.Create(r.Context, h.Domain, h.DB, h.Config, userName, ap.Person, clientCert); err != nil { r.Log.Warn("Failed to create new user", "name", userName, "error", err) w.Status(40, "Failed to create new user") return } - case "y": - if h.Config.EnablePortableActorRegistration { - w.Status(11, "base58-encoded Ed25519 private key or 'generate' to generate") - } else { - w.Status(40, "Registration of portable actors is disabled") - } - return - case "generate": - if !h.Config.EnablePortableActorRegistration { - w.Status(40, "Registration of portable actors is disabled") - 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 } default: - if !h.Config.EnablePortableActorRegistration { - w.Status(40, "Registration of portable actors is disabled") - return - } - key, err := data.DecodeEd25519PrivateKey(r.URL.RawQuery) if err != nil { r.Log.Warn("Failed to decode Ed25519 private key", "name", userName, "error", err) @@ -149,7 +123,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/static/help.gmi b/front/static/help.gmi index b21aa1ba..f5a86ff8 100644 --- a/front/static/help.gmi +++ b/front/static/help.gmi @@ -47,17 +47,6 @@ The Common Name property of the client certificate ("identity") you use will det You can add additional client certificates later, or revoke access for an old certificate. -There are two kinds of accounts: -* Traditional -* Portable +Settings → Data portability allows one to view the private key used to prove the ownership over an account and configure the list of gateways. -Traditional accounts are tied to this particular server, but provide good interoperability. - -Portable accounts may suffer from interoperability issues but have several advantages: -* They're identified by an Ed25519 key pair and not tied to a particular server -* They allow replication of your data across multiple servers ("gateways"); this replication is not 100% reliable, though -* They allow migration away from this server even if it's offline - -Settings → Data portability allows one to view the private key used to prove the ownership over a portable account and configure the list of gateways. - -Only one portable account on this server can use a particular private key: if all client certificates associated with your account are lost or expired, you won't be able to register again with same key to restore access to your account. +Only one account on this server can use a particular private key: if all client certificates associated with your account are lost or expired, you won't be able to register again with same key to restore access to your account. diff --git a/front/static/users/help.gmi b/front/static/users/help.gmi index cc798c72..358f2596 100644 --- a/front/static/users/help.gmi +++ b/front/static/users/help.gmi @@ -120,20 +120,9 @@ Polls must have between 2 and {{.Config.PollMaxOptions}} multi-choice options, a The username of a newly created account is the Common Name property of the client certificate used. -There are two kinds of accounts: -* Traditional -* Portable +Settings → Data portability allows one to view the private key used to prove the ownership over an account and configure the list of gateways. -Traditional accounts are tied to this particular server, but provide good interoperability. - -Portable accounts may suffer from interoperability issues but have several advantages: -* They're identified by an Ed25519 key pair and not tied to a particular server -* They allow replication of your data across multiple servers ("gateways"); this replication is not 100% reliable, though -* They allow migration away from this server even if it's offline - -Settings → Data portability allows one to view the private key used to prove the ownership over a portable account and configure the list of gateways. - -Only one portable account on this server can use a particular private key: if all client certificates associated with your account are lost or expired, you won't be able to register again with same key to restore access to your account. +Only one account on this server can use a particular private key: if all client certificates associated with your account are lost or expired, you won't be able to register again with same key to restore access to your account. ## Client Certificates ("Identities") 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..61906220 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}, @@ -199,6 +242,10 @@ func CreatePortable( } // Create creates a new user. +// +// Before v0.21.0, tootik offered users choice between 'traditional' and 'portable' accounts, and this function exists +// only because it's used by tests, to test backward compatibility with older tootik versions and interoperability with +// ActivityPub servers that don't support https://codeberg.org/fediverse/fep/src/branch/main/fep/ef61/fep-ef61.md. func Create(ctx context.Context, domain string, db *sql.DB, cfg *cfg.Config, name string, actorType ap.ActorType, cert *x509.Certificate) (*ap.Actor, [2]httpsig.Key, error) { rsaPriv, rsaPubPem, err := generateRSAKey() if err != nil { @@ -229,9 +276,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/migrations/057_actorgenerator.go b/migrations/057_actorgenerator.go new file mode 100644 index 00000000..769c134c --- /dev/null +++ b/migrations/057_actorgenerator.go @@ -0,0 +1,46 @@ +package migrations + +import ( + "context" + "crypto/ed25519" + "database/sql" + + "github.com/dimkr/tootik/ap" + "github.com/dimkr/tootik/httpsig" + "github.com/dimkr/tootik/proof" +) + +func actorgenerator(ctx context.Context, domain string, tx *sql.Tx) error { + var actor ap.Actor + var ed25519PrivKey []byte + if err := tx.QueryRowContext( + ctx, + `SELECT JSON(actor), ed25519privkey FROM persons WHERE ed25519privkey IS NOT NULL AND actor->>'$.preferredUsername' = 'actor'`, + ).Scan(&actor, &ed25519PrivKey); err != nil { + return err + } + + 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", + }, + } + + var err error + 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 + } + + 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/register_test.go b/test/register_test.go index 4697ad06..b6ff1f36 100644 --- a/test/register_test.go +++ b/test/register_test.go @@ -585,7 +585,7 @@ func TestRegister_NoCertificate(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) handler, err := front.NewHandler(domain, &cfg, fed.NewResolver(nil, domain, &cfg, &http.Client{}, db), db, &inbox.Inbox{}) @@ -665,7 +665,7 @@ func TestRegister_HappyFlow(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) handler, err := front.NewHandler(domain, &cfg, fed.NewResolver(nil, domain, &cfg, &http.Client{}, db), db, &inbox.Inbox{}) @@ -746,7 +746,7 @@ func TestRegister_AlreadyRegistered(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) _, _, err = user.Create(context.Background(), domain, db, &cfg, "erin", ap.Person, erinKeyPair.Leaf) @@ -835,7 +835,7 @@ func TestRegister_Twice(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) handler, err := front.NewHandler(domain, &cfg, fed.NewResolver(nil, domain, &cfg, &http.Client{}, db), db, &inbox.Inbox{}) @@ -931,7 +931,7 @@ func TestRegister_Throttling(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) handler, err := front.NewHandler(domain, &cfg, fed.NewResolver(nil, domain, &cfg, &http.Client{}, db), db, &inbox.Inbox{}) @@ -1027,7 +1027,7 @@ func TestRegister_Throttling30Minutes(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) handler, err := front.NewHandler(domain, &cfg, fed.NewResolver(nil, domain, &cfg, &http.Client{}, db), db, &inbox.Inbox{}) @@ -1126,7 +1126,7 @@ func TestRegister_Throttling1Hour(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) handler, err := front.NewHandler(domain, &cfg, fed.NewResolver(nil, domain, &cfg, &http.Client{}, db), db, &inbox.Inbox{}) @@ -1206,19 +1206,19 @@ func TestRegister_TwoCertificates(t *testing.T) { clientCfg *tls.Config }{ {"gemini://localhost.localdomain:8965/users\r\n", "^30 /users/register\r\n$", &clientCfg}, - {"gemini://localhost.localdomain:8965/users/register\r\n", "^30 /users\r\n$", &clientCfg}, + {"gemini://localhost.localdomain:8965/users/register?generate\r\n", "^30 /users\r\n$", &clientCfg}, {"gemini://localhost.localdomain:8965/users\r\n", "^20 text/gemini\r\n.+", &clientCfg}, - {"gemini://localhost.localdomain:8965/users/register\r\n", "^40 Already registered as erin\r\n$", &clientCfg}, + {"gemini://localhost.localdomain:8965/users/register?generate\r\n", "^40 Already registered as erin\r\n$", &clientCfg}, {"gemini://localhost.localdomain:8965/users\r\n", "^30 /users/register\r\n$", &otherClientCfg}, - {"gemini://localhost.localdomain:8965/users/register\r\n", "^30 /users\r\n$", &otherClientCfg}, + {"gemini://localhost.localdomain:8965/users/register?generate\r\n", "^30 /users\r\n$", &otherClientCfg}, {"gemini://localhost.localdomain:8965/users\r\n", "^40 Client certificate is awaiting approval\r\n$", &otherClientCfg}, - {"gemini://localhost.localdomain:8965/users/register\r\n", "^40 Client certificate is awaiting approval\r\n$", &otherClientCfg}, + {"gemini://localhost.localdomain:8965/users/register?generate\r\n", "^40 Client certificate is awaiting approval\r\n$", &otherClientCfg}, {fmt.Sprintf("gemini://localhost.localdomain:8965/users/certificates/approve/%s\r\n", erinOtherCertHash), "^30 /users/certificates\r\n$", &clientCfg}, - {"gemini://localhost.localdomain:8965/users/register\r\n", "^40 Already registered as erin\r\n$", &otherClientCfg}, + {"gemini://localhost.localdomain:8965/users/register?generate\r\n", "^40 Already registered as erin\r\n$", &otherClientCfg}, {"gemini://localhost.localdomain:8965/users\r\n", "^20 text/gemini\r\n.+", &otherClientCfg}, {fmt.Sprintf("gemini://localhost.localdomain:8965/users/certificates/revoke/%s\r\n", erinCertHash), "^30 /users/certificates\r\n$", &otherClientCfg}, {"gemini://localhost.localdomain:8965/users\r\n", "^30 /users/register\r\n$", &clientCfg}, - {"gemini://localhost.localdomain:8965/users/register\r\n", "^30 /users\r\n$", &clientCfg}, + {"gemini://localhost.localdomain:8965/users/register?generate\r\n", "^30 /users\r\n$", &clientCfg}, {"gemini://localhost.localdomain:8965/users\r\n", "^40 Client certificate is awaiting approval\r\n$", &clientCfg}, {"gemini://localhost.localdomain:8965/users\r\n", "^20 text/gemini\r\n.+", &otherClientCfg}, } { @@ -1323,7 +1323,7 @@ func TestRegister_ForbiddenUserName(t *testing.T) { }) wg.Wait() - _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register\r\n")) + _, err = tlsReader.Write([]byte("gemini://localhost.localdomain:8965/users/register?generate\r\n")) assert.NoError(err) handler, err := front.NewHandler(domain, &cfg, fed.NewResolver(nil, domain, &cfg, &http.Client{}, db), db, &inbox.Inbox{}) 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, } }