diff --git a/FEDERATION.md b/FEDERATION.md index b6e923ac..e2722dfc 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -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..f7715bc0 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 diff --git a/cluster/followers_sync_test.go b/cluster/followers_sync_test.go index 5d688bd2..46c49003 100644 --- a/cluster/followers_sync_test.go +++ b/cluster/followers_sync_test.go @@ -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) @@ -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) 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/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..4e951fc5 100644 --- a/cluster/server.go +++ b/cluster/server.go @@ -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") diff --git a/fed/listener.go b/fed/listener.go index d09d6ee5..319aab6d 100644 --- a/fed/listener.go +++ b/fed/listener.go @@ -58,7 +58,7 @@ 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/{username}", l.handleInbox) // for compatibility with actors created by tootik<0.21.0 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("GET /outbox/{username}", l.handleOutbox) diff --git a/front/register.go b/front/register.go index 9fa3a84c..64d184eb 100644 --- a/front/register.go +++ b/front/register.go @@ -93,36 +93,22 @@ 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) @@ -137,11 +123,6 @@ func (h *Handler) register(w text.Writer, r *Request, args ...string) { } 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) 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/create.go b/front/user/create.go index ec4dddf4..6d4878b5 100644 --- a/front/user/create.go +++ b/front/user/create.go @@ -199,6 +199,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 { 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{})