mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-10 06:54:16 +00:00
[feature] Allow admins to expire remote public keys; refetch expired keys on demand (#2183)
This commit is contained in:
parent
2cac5a4613
commit
4b594516ec
23 changed files with 841 additions and 117 deletions
|
@ -445,6 +445,19 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
x-go-name: AdminAccountInfo
|
x-go-name: AdminAccountInfo
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
adminActionResponse:
|
||||||
|
description: |-
|
||||||
|
AdminActionResponse models the server
|
||||||
|
response to an admin action.
|
||||||
|
properties:
|
||||||
|
action_id:
|
||||||
|
description: Internal ID of the action.
|
||||||
|
example: 01H9QG6TZ9W5P0402VFRVM17TH
|
||||||
|
type: string
|
||||||
|
x-go-name: ActionID
|
||||||
|
type: object
|
||||||
|
x-go-name: AdminActionResponse
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
adminEmoji:
|
adminEmoji:
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
|
@ -1018,6 +1031,16 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
x-go-name: DomainBlockCreateRequest
|
x-go-name: DomainBlockCreateRequest
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
domainKeysExpireRequest:
|
||||||
|
properties:
|
||||||
|
domain:
|
||||||
|
description: hostname/domain to expire keys for.
|
||||||
|
type: string
|
||||||
|
x-go-name: Domain
|
||||||
|
title: DomainBlockCreateRequest is the form submitted as a POST to /api/v1/admin/domain_keys_expire to expire a domain's public keys.
|
||||||
|
type: object
|
||||||
|
x-go-name: DomainKeysExpireRequest
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
emoji:
|
emoji:
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
|
@ -4103,6 +4126,56 @@ paths:
|
||||||
summary: View domain block with the given ID.
|
summary: View domain block with the given ID.
|
||||||
tags:
|
tags:
|
||||||
- admin
|
- admin
|
||||||
|
/api/v1/admin/domain_keys_expire:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- multipart/form-data
|
||||||
|
description: |-
|
||||||
|
This is useful in cases where the remote domain has had to rotate their keys for whatever
|
||||||
|
reason (security issue, data leak, routine safety procedure, etc), and your instance can no
|
||||||
|
longer communicate with theirs properly using cached keys. A key marked as expired in this way
|
||||||
|
will be lazily refetched next time a request is made to your instance signed by the owner of that
|
||||||
|
key, so no further action should be required in order to reestablish communication with that domain.
|
||||||
|
|
||||||
|
This endpoint is explicitly not for rotating your *own* keys, it only works for remote instances.
|
||||||
|
|
||||||
|
Using this endpoint to expire keys for a domain that hasn't rotated all of their keys is not
|
||||||
|
harmful and won't break federation, but it is pointless and will cause unnecessary requests to
|
||||||
|
be performed.
|
||||||
|
operationId: domainKeysExpire
|
||||||
|
parameters:
|
||||||
|
- description: Domain to expire keys for.
|
||||||
|
example: example.org
|
||||||
|
in: formData
|
||||||
|
name: domain
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"202":
|
||||||
|
description: Request accepted and will be processed. Check the logs for progress / errors.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/adminActionResponse'
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"409":
|
||||||
|
description: 'Conflict: There is already an admin action running that conflicts with this action. Check the error message in the response body for more information. This is a temporary error; it should be possible to process this action if you try again in a bit.'
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- admin
|
||||||
|
summary: Force expiry of cached public keys for all accounts on the given domain stored in your database.
|
||||||
|
tags:
|
||||||
|
- admin
|
||||||
/api/v1/admin/email/test:
|
/api/v1/admin/email/test:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
@ -31,6 +31,7 @@ const (
|
||||||
EmojiCategoriesPath = EmojiPath + "/categories"
|
EmojiCategoriesPath = EmojiPath + "/categories"
|
||||||
DomainBlocksPath = BasePath + "/domain_blocks"
|
DomainBlocksPath = BasePath + "/domain_blocks"
|
||||||
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
|
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
|
||||||
|
DomainKeysExpirePath = BasePath + "/domain_keys_expire"
|
||||||
AccountsPath = BasePath + "/accounts"
|
AccountsPath = BasePath + "/accounts"
|
||||||
AccountsPathWithID = AccountsPath + "/:" + IDKey
|
AccountsPathWithID = AccountsPath + "/:" + IDKey
|
||||||
AccountsActionPath = AccountsPathWithID + "/action"
|
AccountsActionPath = AccountsPathWithID + "/action"
|
||||||
|
@ -83,6 +84,9 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
||||||
attachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
|
attachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
|
||||||
attachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler)
|
attachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler)
|
||||||
|
|
||||||
|
// domain maintenance stuff
|
||||||
|
attachHandler(http.MethodPost, DomainKeysExpirePath, m.DomainKeysExpirePOSTHandler)
|
||||||
|
|
||||||
// accounts stuff
|
// accounts stuff
|
||||||
attachHandler(http.MethodPost, AccountsActionPath, m.AccountActionPOSTHandler)
|
attachHandler(http.MethodPost, AccountsActionPath, m.AccountActionPOSTHandler)
|
||||||
|
|
||||||
|
|
149
internal/api/client/admin/domainkeysexpire.go
Normal file
149
internal/api/client/admin/domainkeysexpire.go
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainKeysExpirePOSTHandler swagger:operation POST /api/v1/admin/domain_keys_expire domainKeysExpire
|
||||||
|
//
|
||||||
|
// Force expiry of cached public keys for all accounts on the given domain stored in your database.
|
||||||
|
//
|
||||||
|
// This is useful in cases where the remote domain has had to rotate their keys for whatever
|
||||||
|
// reason (security issue, data leak, routine safety procedure, etc), and your instance can no
|
||||||
|
// longer communicate with theirs properly using cached keys. A key marked as expired in this way
|
||||||
|
// will be lazily refetched next time a request is made to your instance signed by the owner of that
|
||||||
|
// key, so no further action should be required in order to reestablish communication with that domain.
|
||||||
|
//
|
||||||
|
// This endpoint is explicitly not for rotating your *own* keys, it only works for remote instances.
|
||||||
|
//
|
||||||
|
// Using this endpoint to expire keys for a domain that hasn't rotated all of their keys is not
|
||||||
|
// harmful and won't break federation, but it is pointless and will cause unnecessary requests to
|
||||||
|
// be performed.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - admin
|
||||||
|
//
|
||||||
|
// consumes:
|
||||||
|
// - multipart/form-data
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// parameters:
|
||||||
|
// -
|
||||||
|
// name: domain
|
||||||
|
// in: formData
|
||||||
|
// description: Domain to expire keys for.
|
||||||
|
// example: example.org
|
||||||
|
// type: string
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - admin
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '202':
|
||||||
|
// description: >-
|
||||||
|
// Request accepted and will be processed.
|
||||||
|
// Check the logs for progress / errors.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/adminActionResponse"
|
||||||
|
// '400':
|
||||||
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '409':
|
||||||
|
// description: >-
|
||||||
|
// Conflict: There is already an admin action running that conflicts with this action.
|
||||||
|
// Check the error message in the response body for more information. This is a temporary
|
||||||
|
// error; it should be possible to process this action if you try again in a bit.
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
|
func (m *Module) DomainKeysExpirePOSTHandler(c *gin.Context) {
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*authed.User.Admin {
|
||||||
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := new(apimodel.DomainKeysExpireRequest)
|
||||||
|
if err := c.ShouldBind(form); err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateDomainKeysExpire(form); err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actionID, errWithCode := m.processor.Admin().DomainKeysExpire(
|
||||||
|
c.Request.Context(),
|
||||||
|
authed.Account,
|
||||||
|
form.Domain,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusAccepted, &apimodel.AdminActionResponse{ActionID: actionID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDomainKeysExpire(form *apimodel.DomainKeysExpireRequest) error {
|
||||||
|
form.Domain = strings.TrimSpace(form.Domain)
|
||||||
|
if form.Domain == "" {
|
||||||
|
return errors.New("no domain given")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Domain == config.GetHost() || form.Domain == config.GetAccountDomain() {
|
||||||
|
return errors.New("provided domain was this domain, but must be a remote domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -178,6 +178,17 @@ type AdminActionRequest struct {
|
||||||
TargetID string `form:"-" json:"-" xml:"-"`
|
TargetID string `form:"-" json:"-" xml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminActionResponse models the server
|
||||||
|
// response to an admin action.
|
||||||
|
//
|
||||||
|
// swagger:model adminActionResponse
|
||||||
|
type AdminActionResponse struct {
|
||||||
|
// Internal ID of the action.
|
||||||
|
//
|
||||||
|
// example: 01H9QG6TZ9W5P0402VFRVM17TH
|
||||||
|
ActionID string `json:"action_id"`
|
||||||
|
}
|
||||||
|
|
||||||
// MediaCleanupRequest models admin media cleanup parameters
|
// MediaCleanupRequest models admin media cleanup parameters
|
||||||
//
|
//
|
||||||
// swagger:parameters mediaCleanup
|
// swagger:parameters mediaCleanup
|
||||||
|
|
|
@ -79,3 +79,11 @@ type DomainBlockCreateRequest struct {
|
||||||
// public comment on the reason for the domain block
|
// public comment on the reason for the domain block
|
||||||
PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"`
|
PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DomainBlockCreateRequest is the form submitted as a POST to /api/v1/admin/domain_keys_expire to expire a domain's public keys.
|
||||||
|
//
|
||||||
|
// swagger:model domainKeysExpireRequest
|
||||||
|
type DomainKeysExpireRequest struct {
|
||||||
|
// hostname/domain to expire keys for.
|
||||||
|
Domain string `form:"domain" json:"domain" xml:"domain"`
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
up := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
_, err := db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? TIMESTAMPTZ", bun.Ident("accounts"), bun.Ident("public_key_expires_at"))
|
||||||
|
if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
down := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Migrations.Register(up, down); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,14 +25,17 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
"codeberg.org/gruf/go-kv"
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
"github.com/superseriousbusiness/activity/streams"
|
"github.com/superseriousbusiness/activity/streams"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,11 +48,47 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PubKeyAuth models authorization information for a remote
|
||||||
|
// Actor making a signed HTTP request to this GtS instance
|
||||||
|
// using a public key.
|
||||||
|
type PubKeyAuth struct {
|
||||||
|
// CachedPubKey is the public key found in the db
|
||||||
|
// for the Actor whose request we're now authenticating.
|
||||||
|
// Will be set only in cases where we had the Owner
|
||||||
|
// of the key stored in the database already.
|
||||||
|
CachedPubKey *rsa.PublicKey
|
||||||
|
|
||||||
|
// FetchedPubKey is an up-to-date public key fetched
|
||||||
|
// from the remote instance. Will be set in cases
|
||||||
|
// where EITHER we hadn't seen the Actor before whose
|
||||||
|
// request we're now authenticating, OR a CachedPubKey
|
||||||
|
// was found in our database, but was expired.
|
||||||
|
FetchedPubKey *rsa.PublicKey
|
||||||
|
|
||||||
|
// OwnerURI is the ActivityPub id of the owner of
|
||||||
|
// the public key used to sign the request we're
|
||||||
|
// now authenticating. This will always be set
|
||||||
|
// even if Owner isn't, so that callers can use
|
||||||
|
// this URI to go fetch the Owner from remote.
|
||||||
|
OwnerURI *url.URL
|
||||||
|
|
||||||
|
// Owner is the account corresponding to OwnerURI.
|
||||||
|
//
|
||||||
|
// Owner will only be defined if the account who
|
||||||
|
// owns the public key was already cached in the
|
||||||
|
// database when we received the request we're now
|
||||||
|
// authenticating (ie., we've seen it before).
|
||||||
|
//
|
||||||
|
// If it's not defined, callers should use OwnerURI
|
||||||
|
// to go and dereference it.
|
||||||
|
Owner *gtsmodel.Account
|
||||||
|
}
|
||||||
|
|
||||||
// AuthenticateFederatedRequest authenticates any kind of incoming federated
|
// AuthenticateFederatedRequest authenticates any kind of incoming federated
|
||||||
// request from a remote server. This includes things like GET requests for
|
// request from a remote server. This includes things like GET requests for
|
||||||
// dereferencing our users or statuses etc, and POST requests for delivering
|
// dereferencing our users or statuses etc, and POST requests for delivering
|
||||||
// new Activities. The function returns the URL of the owner of the public key
|
// new Activities. The function returns details of the public key(s) used to
|
||||||
// used in the requesting http signature.
|
// authenticate the requesting http signature.
|
||||||
//
|
//
|
||||||
// 'Authenticate' in this case is defined as making sure that the http request
|
// 'Authenticate' in this case is defined as making sure that the http request
|
||||||
// is actually signed by whoever claims to have signed it, by fetching the public
|
// is actually signed by whoever claims to have signed it, by fetching the public
|
||||||
|
@ -70,7 +109,7 @@ var (
|
||||||
// Also note that this function *does not* dereference the remote account that
|
// Also note that this function *does not* dereference the remote account that
|
||||||
// the signature key is associated with. Other functions should use the returned
|
// the signature key is associated with. Other functions should use the returned
|
||||||
// URL to dereference the remote account, if required.
|
// URL to dereference the remote account, if required.
|
||||||
func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*url.URL, gtserror.WithCode) {
|
func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedUsername string) (*PubKeyAuth, gtserror.WithCode) {
|
||||||
// Thanks to the signature check middleware,
|
// Thanks to the signature check middleware,
|
||||||
// we should already have an http signature
|
// we should already have an http signature
|
||||||
// verifier set on the context. If we don't,
|
// verifier set on the context. If we don't,
|
||||||
|
@ -102,10 +141,10 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
||||||
// so now we need to validate the signature.
|
// so now we need to validate the signature.
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pubKeyIDStr = pubKeyID.String()
|
pubKeyIDStr = pubKeyID.String()
|
||||||
requestingAccountURI *url.URL
|
local = (pubKeyID.Host == config.GetHost())
|
||||||
pubKey interface{}
|
pubKeyAuth *PubKeyAuth
|
||||||
errWithCode gtserror.WithCode
|
errWithCode gtserror.WithCode
|
||||||
)
|
)
|
||||||
|
|
||||||
l := log.
|
l := log.
|
||||||
|
@ -115,37 +154,49 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
||||||
{"pubKeyID", pubKeyIDStr},
|
{"pubKeyID", pubKeyIDStr},
|
||||||
}...)
|
}...)
|
||||||
|
|
||||||
if pubKeyID.Host == config.GetHost() {
|
if local {
|
||||||
l.Trace("public key is ours, no dereference needed")
|
l.Trace("public key is local, no dereference needed")
|
||||||
requestingAccountURI, pubKey, errWithCode = f.derefDBOnly(ctx, pubKeyIDStr)
|
pubKeyAuth, errWithCode = f.derefPubKeyDBOnly(ctx, pubKeyIDStr)
|
||||||
} else {
|
} else {
|
||||||
l.Trace("public key is not ours, checking if we need to dereference")
|
l.Trace("public key is remote, checking if we need to dereference")
|
||||||
requestingAccountURI, pubKey, errWithCode = f.deref(ctx, requestedUsername, pubKeyIDStr, pubKeyID)
|
pubKeyAuth, errWithCode = f.derefPubKey(ctx, requestedUsername, pubKeyIDStr, pubKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure public key now defined.
|
if local && pubKeyAuth == nil {
|
||||||
if pubKey == nil {
|
// We signed this request, apparently, but
|
||||||
err := gtserror.New("public key was nil")
|
// local lookup didn't find anything. This
|
||||||
|
// is an almost impossible error condition!
|
||||||
|
err := gtserror.Newf("local public key %s could not be found; "+
|
||||||
|
"has the account been manually removed from the db?", pubKeyIDStr)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to authenticate using permitted algorithms in
|
// Try to authenticate using permitted algorithms in
|
||||||
// order of most -> least common. Return OK as soon
|
// order of most -> least common, checking each defined
|
||||||
// as one passes.
|
// pubKey for this Actor. Return OK as soon as one passes.
|
||||||
for _, algo := range signingAlgorithms {
|
for _, pubKey := range [2]*rsa.PublicKey{
|
||||||
l.Tracef("trying %s", algo)
|
pubKeyAuth.FetchedPubKey,
|
||||||
|
pubKeyAuth.CachedPubKey,
|
||||||
err := verifier.Verify(pubKey, algo)
|
} {
|
||||||
if err == nil {
|
if pubKey == nil {
|
||||||
l.Tracef("authentication PASSED with %s", algo)
|
continue
|
||||||
return requestingAccountURI, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Tracef("authentication NOT PASSED with %s: %q", algo, err)
|
for _, algo := range signingAlgorithms {
|
||||||
|
l.Tracef("trying %s", algo)
|
||||||
|
|
||||||
|
err := verifier.Verify(pubKey, algo)
|
||||||
|
if err == nil {
|
||||||
|
l.Tracef("authentication PASSED with %s", algo)
|
||||||
|
return pubKeyAuth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("authentication NOT PASSED with %s: %q", algo, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point no algorithms passed.
|
// At this point no algorithms passed.
|
||||||
|
@ -157,36 +208,52 @@ func (f *federator) AuthenticateFederatedRequest(ctx context.Context, requestedU
|
||||||
return nil, gtserror.NewErrorUnauthorized(err, err.Error())
|
return nil, gtserror.NewErrorUnauthorized(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// derefDBOnly tries to dereference the given public
|
// derefPubKeyDBOnly tries to dereference the given
|
||||||
// key using only entries already in the database.
|
// pubKey using only entries already in the database.
|
||||||
func (f *federator) derefDBOnly(
|
//
|
||||||
|
// In case of a db or URL error, will return the error.
|
||||||
|
//
|
||||||
|
// In case an entry for the pubKey owner just doesn't
|
||||||
|
// exist in the db (yet), will return nil, nil.
|
||||||
|
func (f *federator) derefPubKeyDBOnly(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pubKeyIDStr string,
|
pubKeyIDStr string,
|
||||||
) (*url.URL, interface{}, gtserror.WithCode) {
|
) (*PubKeyAuth, gtserror.WithCode) {
|
||||||
reqAcct, err := f.db.GetAccountByPubkeyID(ctx, pubKeyIDStr)
|
owner, err := f.db.GetAccountByPubkeyID(ctx, pubKeyIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// We don't have this
|
||||||
|
// account stored (yet).
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
err = gtserror.Newf("db error getting account with pubKeyID %s: %w", pubKeyIDStr, err)
|
err = gtserror.Newf("db error getting account with pubKeyID %s: %w", pubKeyIDStr, err)
|
||||||
return nil, nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqAcctURI, err := url.Parse(reqAcct.URI)
|
ownerURI, err := url.Parse(owner.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error parsing account uri with pubKeyID %s: %w", pubKeyIDStr, err)
|
err = gtserror.Newf("error parsing account uri with pubKeyID %s: %w", pubKeyIDStr, err)
|
||||||
return nil, nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return reqAcctURI, reqAcct.PublicKey, nil
|
return &PubKeyAuth{
|
||||||
|
CachedPubKey: owner.PublicKey,
|
||||||
|
OwnerURI: ownerURI,
|
||||||
|
Owner: owner,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deref tries to dereference the given public key by first
|
// derefPubKey tries to dereference the given public key by first
|
||||||
// checking in the database, and then (if no entries found)
|
// checking in the database, and then (if no entry found, or entry
|
||||||
// calling the remote pub key URI and extracting the key.
|
// found but pubKey expired) calling the remote pub key URI and
|
||||||
func (f *federator) deref(
|
// extracting the key.
|
||||||
|
func (f *federator) derefPubKey(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requestedUsername string,
|
requestedUsername string,
|
||||||
pubKeyIDStr string,
|
pubKeyIDStr string,
|
||||||
pubKeyID *url.URL,
|
pubKeyID *url.URL,
|
||||||
) (*url.URL, interface{}, gtserror.WithCode) {
|
) (*PubKeyAuth, gtserror.WithCode) {
|
||||||
l := log.
|
l := log.
|
||||||
WithContext(ctx).
|
WithContext(ctx).
|
||||||
WithFields(kv.Fields{
|
WithFields(kv.Fields{
|
||||||
|
@ -196,42 +263,101 @@ func (f *federator) deref(
|
||||||
|
|
||||||
// Try a database only deref first. We may already
|
// Try a database only deref first. We may already
|
||||||
// have the requesting account cached locally.
|
// have the requesting account cached locally.
|
||||||
reqAcctURI, pubKey, errWithCode := f.derefDBOnly(ctx, pubKeyIDStr)
|
pubKeyAuth, errWithCode := f.derefPubKeyDBOnly(ctx, pubKeyIDStr)
|
||||||
if errWithCode == nil {
|
if errWithCode != nil {
|
||||||
l.Trace("public key cached, no dereference needed")
|
return nil, errWithCode
|
||||||
return reqAcctURI, pubKey, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Trace("public key not cached, trying dereference")
|
var (
|
||||||
|
// Just haven't seen this
|
||||||
|
// Actor + their pubkey yet.
|
||||||
|
uncached = (pubKeyAuth == nil)
|
||||||
|
|
||||||
|
// Have seen this Actor + their
|
||||||
|
// pubkey but latter is now expired.
|
||||||
|
expired = (!uncached && pubKeyAuth.Owner.PubKeyExpired())
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case uncached:
|
||||||
|
l.Trace("public key was not cached, trying dereference of public key")
|
||||||
|
case !expired:
|
||||||
|
l.Trace("public key cached and up to date, no dereference needed")
|
||||||
|
return pubKeyAuth, nil
|
||||||
|
case expired:
|
||||||
|
// This is fairly rare and it may be helpful for
|
||||||
|
// admins to see what's going on, so log at info.
|
||||||
|
l.Infof(
|
||||||
|
"public key was cached, but expired at %s, trying dereference of new public key",
|
||||||
|
pubKeyAuth.Owner.PublicKeyExpiresAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// If we've tried to get this account before and we
|
// If we've tried to get this account before and we
|
||||||
// now have a tombstone for it (ie., it's been deleted
|
// now have a tombstone for it (ie., it's been deleted
|
||||||
// from remote), don't try to dereference it again.
|
// from remote), don't try to dereference it again.
|
||||||
gone, err := f.CheckGone(ctx, pubKeyID)
|
gone, err := f.CheckGone(ctx, pubKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error checking for tombstone for %s: %w", pubKeyIDStr, err)
|
err := gtserror.Newf("error checking for tombstone (%s): %w", pubKeyIDStr, err)
|
||||||
return nil, nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if gone {
|
if gone {
|
||||||
err := gtserror.Newf("account with public key %s is gone", pubKeyIDStr)
|
err := gtserror.Newf("account with public key is gone (%s)", pubKeyIDStr)
|
||||||
return nil, nil, gtserror.NewErrorGone(err)
|
return nil, gtserror.NewErrorGone(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make an http call to get the pubkey.
|
// Make an http call to get the (refreshed) pubkey.
|
||||||
pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUsername, pubKeyID)
|
pubKeyBytes, errWithCode := f.callForPubKey(ctx, requestedUsername, pubKeyID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the key and the owner from the response.
|
// Extract the key and the owner from the response.
|
||||||
pubKey, pubKeyOwner, err := parsePubKeyBytes(ctx, pubKeyBytes, pubKeyID)
|
pubKey, pubKeyOwner, err := parsePubKeyBytes(ctx, pubKeyBytes, pubKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("error parsing public key %s: %w", pubKeyID, err)
|
err := fmt.Errorf("error parsing public key (%s): %w", pubKeyID, err)
|
||||||
return nil, nil, gtserror.NewErrorUnauthorized(err)
|
return nil, gtserror.NewErrorUnauthorized(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pubKeyOwner, pubKey, nil
|
if !expired {
|
||||||
|
// PubKeyResponse was nil before because
|
||||||
|
// we had nothing cached; return the key
|
||||||
|
// we just fetched, and nothing else.
|
||||||
|
return &PubKeyAuth{
|
||||||
|
FetchedPubKey: pubKey,
|
||||||
|
OwnerURI: pubKeyOwner,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add newly-fetched key to response.
|
||||||
|
pubKeyAuth.FetchedPubKey = pubKey
|
||||||
|
|
||||||
|
// If key was expired, that means we already
|
||||||
|
// had an owner stored for it locally. Since
|
||||||
|
// we now successfully refreshed the pub key,
|
||||||
|
// we should update the account to reflect that.
|
||||||
|
ownerAcct := pubKeyAuth.Owner
|
||||||
|
ownerAcct.PublicKey = pubKeyAuth.FetchedPubKey
|
||||||
|
ownerAcct.PublicKeyExpiresAt = time.Time{}
|
||||||
|
|
||||||
|
l.Info("obtained a new public key to replace expired key, caching now; " +
|
||||||
|
"authorization for this request will be attempted with both old and new keys")
|
||||||
|
|
||||||
|
if err := f.db.UpdateAccount(
|
||||||
|
ctx,
|
||||||
|
ownerAcct,
|
||||||
|
"public_key",
|
||||||
|
"public_key_expires_at",
|
||||||
|
); err != nil {
|
||||||
|
err := gtserror.Newf("db error updating account with refreshed public key (%s): %w", pubKeyIDStr, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return both new and cached (now
|
||||||
|
// expired) keys, authentication
|
||||||
|
// will be attempted with both.
|
||||||
|
return pubKeyAuth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// callForPubKey handles the nitty gritty of actually
|
// callForPubKey handles the nitty gritty of actually
|
||||||
|
|
|
@ -209,7 +209,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check who's trying to deliver to us by inspecting the http signature.
|
// Check who's trying to deliver to us by inspecting the http signature.
|
||||||
pubKeyOwner, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username)
|
pubKeyAuth, errWithCode := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
switch errWithCode.Code() {
|
switch errWithCode.Code() {
|
||||||
case http.StatusUnauthorized, http.StatusForbidden, http.StatusBadRequest:
|
case http.StatusUnauthorized, http.StatusForbidden, http.StatusBadRequest:
|
||||||
|
@ -232,12 +232,14 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pubKeyOwnerURI := pubKeyAuth.OwnerURI
|
||||||
|
|
||||||
// Authentication has passed, check if we need to create a
|
// Authentication has passed, check if we need to create a
|
||||||
// new instance entry for the Host of the requesting account.
|
// new instance entry for the Host of the requesting account.
|
||||||
if _, err := f.db.GetInstance(ctx, pubKeyOwner.Host); err != nil {
|
if _, err := f.db.GetInstance(ctx, pubKeyOwnerURI.Host); err != nil {
|
||||||
if !errors.Is(err, db.ErrNoEntries) {
|
if !errors.Is(err, db.ErrNoEntries) {
|
||||||
// There's been an actual error.
|
// There's been an actual error.
|
||||||
err = gtserror.Newf("error getting instance %s: %w", pubKeyOwner.Host, err)
|
err = gtserror.Newf("error getting instance %s: %w", pubKeyOwnerURI.Host, err)
|
||||||
return ctx, false, err
|
return ctx, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,17 +249,17 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
gtscontext.SetFastFail(ctx),
|
gtscontext.SetFastFail(ctx),
|
||||||
username,
|
username,
|
||||||
&url.URL{
|
&url.URL{
|
||||||
Scheme: pubKeyOwner.Scheme,
|
Scheme: pubKeyOwnerURI.Scheme,
|
||||||
Host: pubKeyOwner.Host,
|
Host: pubKeyOwnerURI.Host,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error dereferencing instance %s: %w", pubKeyOwner.Host, err)
|
err = gtserror.Newf("error dereferencing instance %s: %w", pubKeyOwnerURI.Host, err)
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.db.PutInstance(ctx, instance); err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
if err := f.db.PutInstance(ctx, instance); err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
||||||
err = gtserror.Newf("error inserting instance entry for %s: %w", pubKeyOwner.Host, err)
|
err = gtserror.Newf("error inserting instance entry for %s: %w", pubKeyOwnerURI.Host, err)
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,7 +270,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
requestingAccount, _, err := f.GetAccountByURI(
|
requestingAccount, _, err := f.GetAccountByURI(
|
||||||
gtscontext.SetFastFail(ctx),
|
gtscontext.SetFastFail(ctx),
|
||||||
username,
|
username,
|
||||||
pubKeyOwner,
|
pubKeyOwnerURI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if gtserror.StatusCode(err) == http.StatusGone {
|
if gtserror.StatusCode(err) == http.StatusGone {
|
||||||
|
@ -282,7 +284,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
return ctx, false, nil
|
return ctx, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = gtserror.Newf("couldn't get requesting account %s: %w", pubKeyOwner, err)
|
err = gtserror.Newf("couldn't get requesting account %s: %w", pubKeyOwnerURI, err)
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -257,6 +257,33 @@ func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() {
|
||||||
suite.Equal(http.StatusOK, code)
|
suite.Equal(http.StatusOK, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInboxKeyExpired() {
|
||||||
|
var (
|
||||||
|
ctx = context.Background()
|
||||||
|
activity = suite.testActivities["dm_for_zork"]
|
||||||
|
receivingAccount = suite.testAccounts["local_account_1"]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update remote account to mark key as expired.
|
||||||
|
remoteAcct := >smodel.Account{}
|
||||||
|
*remoteAcct = *suite.testAccounts["remote_account_1"]
|
||||||
|
remoteAcct.PublicKeyExpiresAt = testrig.TimeMustParse("2022-06-10T15:22:08Z")
|
||||||
|
if err := suite.state.DB.UpdateAccount(ctx, remoteAcct, "public_key_expires_at"); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, authed, resp, code := suite.authenticatePostInbox(
|
||||||
|
ctx,
|
||||||
|
receivingAccount,
|
||||||
|
activity,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.NotNil(gtscontext.RequestingAccount(ctx))
|
||||||
|
suite.True(authed)
|
||||||
|
suite.Equal([]byte{}, resp)
|
||||||
|
suite.Equal(http.StatusOK, code)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *FederatingProtocolTestSuite) TestAuthenticatePostGoneWithTombstone() {
|
func (suite *FederatingProtocolTestSuite) TestAuthenticatePostGoneWithTombstone() {
|
||||||
var (
|
var (
|
||||||
activity = suite.testActivities["delete_https://somewhere.mysterious/users/rest_in_piss#main-key"]
|
activity = suite.testActivities["delete_https://somewhere.mysterious/users/rest_in_piss#main-key"]
|
||||||
|
|
|
@ -19,7 +19,6 @@ package federation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/activity/pub"
|
"github.com/superseriousbusiness/activity/pub"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
@ -49,7 +48,7 @@ type Federator interface {
|
||||||
// If the request does not pass authentication, or there's a domain block, nil, false, nil will be returned.
|
// If the request does not pass authentication, or there's a domain block, nil, false, nil will be returned.
|
||||||
//
|
//
|
||||||
// If something goes wrong during authentication, nil, false, and an error will be returned.
|
// If something goes wrong during authentication, nil, false, and an error will be returned.
|
||||||
AuthenticateFederatedRequest(ctx context.Context, username string) (*url.URL, gtserror.WithCode)
|
AuthenticateFederatedRequest(ctx context.Context, username string) (*PubKeyAuth, gtserror.WithCode)
|
||||||
|
|
||||||
pub.CommonBehavior
|
pub.CommonBehavior
|
||||||
pub.FederatingProtocol
|
pub.FederatingProtocol
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
package federation_test
|
package federation_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
@ -71,7 +73,14 @@ func (suite *FederatorStandardTestSuite) SetupTest() {
|
||||||
suite.typeconverter,
|
suite.typeconverter,
|
||||||
)
|
)
|
||||||
|
|
||||||
suite.httpClient = testrig.NewMockHTTPClient(nil, "../../testrig/media")
|
// Ensure it's possible to deref
|
||||||
|
// main key of foss satan.
|
||||||
|
fossSatanPerson, err := suite.typeconverter.AccountToAS(context.Background(), suite.testAccounts["remote_account_1"])
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.httpClient = testrig.NewMockHTTPClient(nil, "../../testrig/media", fossSatanPerson)
|
||||||
suite.httpClient.TestRemotePeople = testrig.NewTestFediPeople()
|
suite.httpClient.TestRemotePeople = testrig.NewTestFediPeople()
|
||||||
suite.httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses()
|
suite.httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses()
|
||||||
|
|
||||||
|
|
|
@ -72,9 +72,10 @@ type Account struct {
|
||||||
FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account
|
FollowersURI string `bun:",nullzero,unique"` // URI for getting the followers list of this account
|
||||||
FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account
|
FeaturedCollectionURI string `bun:",nullzero,unique"` // URL for getting the featured collection list of this account
|
||||||
ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account?
|
ActorType string `bun:",nullzero,notnull"` // What type of activitypub actor is this account?
|
||||||
PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for validating activitypub requests, will only be defined for local accounts
|
PrivateKey *rsa.PrivateKey `bun:""` // Privatekey for signing activitypub requests, will only be defined for local accounts
|
||||||
PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for encoding activitypub requests, will be defined for both local and remote accounts
|
PublicKey *rsa.PublicKey `bun:",notnull"` // Publickey for authorizing signed activitypub requests, will be defined for both local and remote accounts
|
||||||
PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key
|
PublicKeyURI string `bun:",nullzero,notnull,unique"` // Web-reachable location of this account's public key
|
||||||
|
PublicKeyExpiresAt time.Time `bun:"type:timestamptz,nullzero"` // PublicKey will expire/has expired at given time, and should be fetched again as appropriate. Only ever set for remote accounts.
|
||||||
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive?
|
SensitizedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account set to have all its media shown as sensitive?
|
||||||
SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
|
SilencedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account silenced (eg., statuses only visible to followers, not public)?
|
||||||
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
|
SuspendedAt time.Time `bun:"type:timestamptz,nullzero"` // When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account)
|
||||||
|
@ -129,6 +130,17 @@ func (a *Account) EmojisPopulated() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PubKeyExpired returns true if the account's public key
|
||||||
|
// has been marked as expired, and the expiry time has passed.
|
||||||
|
func (a *Account) PubKeyExpired() bool {
|
||||||
|
if a == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !a.PublicKeyExpiresAt.IsZero() &&
|
||||||
|
a.PublicKeyExpiresAt.Before(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
// AccountToEmoji is an intermediate struct to facilitate the many2many relationship between an account and one or more emojis.
|
// AccountToEmoji is an intermediate struct to facilitate the many2many relationship between an account and one or more emojis.
|
||||||
type AccountToEmoji struct {
|
type AccountToEmoji struct {
|
||||||
AccountID string `bun:"type:CHAR(26),unique:accountemoji,nullzero,notnull"`
|
AccountID string `bun:"type:CHAR(26),unique:accountemoji,nullzero,notnull"`
|
||||||
|
|
|
@ -72,6 +72,7 @@ const (
|
||||||
AdminActionUnsilence
|
AdminActionUnsilence
|
||||||
AdminActionSuspend
|
AdminActionSuspend
|
||||||
AdminActionUnsuspend
|
AdminActionUnsuspend
|
||||||
|
AdminActionExpireKeys
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t AdminActionType) String() string {
|
func (t AdminActionType) String() string {
|
||||||
|
@ -88,6 +89,8 @@ func (t AdminActionType) String() string {
|
||||||
return "suspend"
|
return "suspend"
|
||||||
case AdminActionUnsuspend:
|
case AdminActionUnsuspend:
|
||||||
return "unsuspend"
|
return "unsuspend"
|
||||||
|
case AdminActionExpireKeys:
|
||||||
|
return "expire-keys"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
@ -107,6 +110,8 @@ func NewAdminActionType(in string) AdminActionType {
|
||||||
return AdminActionSuspend
|
return AdminActionSuspend
|
||||||
case "unsuspend":
|
case "unsuspend":
|
||||||
return AdminActionUnsuspend
|
return AdminActionUnsuspend
|
||||||
|
case "expire-keys":
|
||||||
|
return AdminActionExpireKeys
|
||||||
default:
|
default:
|
||||||
return AdminActionUnknown
|
return AdminActionUnknown
|
||||||
}
|
}
|
||||||
|
|
87
internal/processing/admin/domainkeysexpire.go
Normal file
87
internal/processing/admin/domainkeysexpire.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DomainKeysExpire iterates through all
|
||||||
|
// accounts belonging to the given domain,
|
||||||
|
// and expires the public key of each
|
||||||
|
// account found this way.
|
||||||
|
//
|
||||||
|
// The PublicKey for each account will be
|
||||||
|
// re-fetched next time a signed request
|
||||||
|
// from that account is received.
|
||||||
|
func (p *Processor) DomainKeysExpire(
|
||||||
|
ctx context.Context,
|
||||||
|
adminAcct *gtsmodel.Account,
|
||||||
|
domain string,
|
||||||
|
) (string, gtserror.WithCode) {
|
||||||
|
actionID := id.NewULID()
|
||||||
|
|
||||||
|
// Process key expiration asynchronously.
|
||||||
|
if errWithCode := p.actions.Run(
|
||||||
|
ctx,
|
||||||
|
>smodel.AdminAction{
|
||||||
|
ID: actionID,
|
||||||
|
TargetCategory: gtsmodel.AdminActionCategoryDomain,
|
||||||
|
TargetID: domain,
|
||||||
|
Type: gtsmodel.AdminActionExpireKeys,
|
||||||
|
AccountID: adminAcct.ID,
|
||||||
|
},
|
||||||
|
func(ctx context.Context) gtserror.MultiError {
|
||||||
|
return p.domainKeysExpireSideEffects(ctx, domain)
|
||||||
|
},
|
||||||
|
); errWithCode != nil {
|
||||||
|
return actionID, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) domainKeysExpireSideEffects(ctx context.Context, domain string) gtserror.MultiError {
|
||||||
|
var (
|
||||||
|
expiresAt = time.Now()
|
||||||
|
errs gtserror.MultiError
|
||||||
|
)
|
||||||
|
|
||||||
|
// For each account on this domain, expire
|
||||||
|
// the public key and update the account.
|
||||||
|
if err := p.rangeDomainAccounts(ctx, domain, func(account *gtsmodel.Account) {
|
||||||
|
account.PublicKeyExpiresAt = expiresAt
|
||||||
|
|
||||||
|
if err := p.state.DB.UpdateAccount(
|
||||||
|
ctx,
|
||||||
|
account,
|
||||||
|
"public_key_expires_at",
|
||||||
|
); err != nil {
|
||||||
|
errs.Appendf("db error updating account: %w", err)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
errs.Appendf("db error ranging through accounts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ func (p *Processor) authenticate(ctx context.Context, requestedUsername string)
|
||||||
|
|
||||||
// Ensure request signed, and use signature URI to
|
// Ensure request signed, and use signature URI to
|
||||||
// get requesting account, dereferencing if necessary.
|
// get requesting account, dereferencing if necessary.
|
||||||
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
pubKeyAuth, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, nil, errWithCode
|
return nil, nil, errWithCode
|
||||||
}
|
}
|
||||||
|
@ -56,10 +56,10 @@ func (p *Processor) authenticate(ctx context.Context, requestedUsername string)
|
||||||
requestingAccount, _, err := p.federator.GetAccountByURI(
|
requestingAccount, _, err := p.federator.GetAccountByURI(
|
||||||
gtscontext.SetFastFail(ctx),
|
gtscontext.SetFastFail(ctx),
|
||||||
requestedUsername,
|
requestedUsername,
|
||||||
requestingAccountURI,
|
pubKeyAuth.OwnerURI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error getting account %s: %w", requestingAccountURI, err)
|
err = gtserror.Newf("error getting account %s: %w", pubKeyAuth.OwnerURI, err)
|
||||||
return nil, nil, gtserror.NewErrorUnauthorized(err)
|
return nil, nil, gtserror.NewErrorUnauthorized(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque
|
||||||
// If the request is not on a public key path, we want to
|
// If the request is not on a public key path, we want to
|
||||||
// try to authenticate it before we serve any data, so that
|
// try to authenticate it before we serve any data, so that
|
||||||
// we can serve a more complete profile.
|
// we can serve a more complete profile.
|
||||||
requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
pubKeyAuth, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode // likely 401
|
return nil, errWithCode // likely 401
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque
|
||||||
// Instead, we end up in an 'I'll show you mine if you show me
|
// Instead, we end up in an 'I'll show you mine if you show me
|
||||||
// yours' situation, where we sort of agree to reveal each
|
// yours' situation, where we sort of agree to reveal each
|
||||||
// other's profiles at the same time.
|
// other's profiles at the same time.
|
||||||
if p.federator.Handshaking(requestedUsername, requestingAccountURI) {
|
if p.federator.Handshaking(requestedUsername, pubKeyAuth.OwnerURI) {
|
||||||
return data(person)
|
return data(person)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,10 +98,11 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque
|
||||||
requestingAccount, _, err := p.federator.GetAccountByURI(
|
requestingAccount, _, err := p.federator.GetAccountByURI(
|
||||||
// On a hot path so fail quickly.
|
// On a hot path so fail quickly.
|
||||||
gtscontext.SetFastFail(ctx),
|
gtscontext.SetFastFail(ctx),
|
||||||
requestedUsername, requestingAccountURI,
|
requestedUsername,
|
||||||
|
pubKeyAuth.OwnerURI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error getting account %s: %w", requestingAccountURI, err)
|
err := gtserror.Newf("error getting account %s: %w", pubKeyAuth.OwnerURI, err)
|
||||||
return nil, gtserror.NewErrorUnauthorized(err)
|
return nil, gtserror.NewErrorUnauthorized(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ type MockHTTPClient struct {
|
||||||
// to customize how the client is mocked.
|
// to customize how the client is mocked.
|
||||||
//
|
//
|
||||||
// Note that you should never ever make ACTUAL http calls with this thing.
|
// Note that you should never ever make ACTUAL http calls with this thing.
|
||||||
func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relativeMediaPath string) *MockHTTPClient {
|
func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relativeMediaPath string, extraPeople ...vocab.ActivityStreamsPerson) *MockHTTPClient {
|
||||||
mockHTTPClient := &MockHTTPClient{}
|
mockHTTPClient := &MockHTTPClient{}
|
||||||
|
|
||||||
if do != nil {
|
if do != nil {
|
||||||
|
@ -95,10 +95,13 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
mockHTTPClient.TestTombstones = NewTestTombstones()
|
mockHTTPClient.TestTombstones = NewTestTombstones()
|
||||||
|
|
||||||
mockHTTPClient.do = func(req *http.Request) (*http.Response, error) {
|
mockHTTPClient.do = func(req *http.Request) (*http.Response, error) {
|
||||||
responseCode := http.StatusNotFound
|
var (
|
||||||
responseBytes := []byte(`{"error":"404 not found"}`)
|
responseCode = http.StatusNotFound
|
||||||
responseContentType := applicationJSON
|
responseBytes = []byte(`{"error":"404 not found"}`)
|
||||||
responseContentLength := len(responseBytes)
|
responseContentType = applicationJSON
|
||||||
|
responseContentLength = len(responseBytes)
|
||||||
|
reqURLString = req.URL.String()
|
||||||
|
)
|
||||||
|
|
||||||
if req.Method == http.MethodPost {
|
if req.Method == http.MethodPost {
|
||||||
b, err := io.ReadAll(req.Body)
|
b, err := io.ReadAll(req.Body)
|
||||||
|
@ -106,26 +109,26 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sI, loaded := mockHTTPClient.SentMessages.LoadOrStore(req.URL.String(), [][]byte{b}); loaded {
|
if sI, loaded := mockHTTPClient.SentMessages.LoadOrStore(reqURLString, [][]byte{b}); loaded {
|
||||||
s, ok := sI.([][]byte)
|
s, ok := sI.([][]byte)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("SentMessages entry wasn't [][]byte")
|
panic("SentMessages entry wasn't [][]byte")
|
||||||
}
|
}
|
||||||
s = append(s, b)
|
s = append(s, b)
|
||||||
mockHTTPClient.SentMessages.Store(req.URL.String(), s)
|
mockHTTPClient.SentMessages.Store(reqURLString, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
responseCode = http.StatusOK
|
responseCode = http.StatusOK
|
||||||
responseBytes = []byte(`{"ok":"accepted"}`)
|
responseBytes = []byte(`{"ok":"accepted"}`)
|
||||||
responseContentType = applicationJSON
|
responseContentType = applicationJSON
|
||||||
responseContentLength = len(responseBytes)
|
responseContentLength = len(responseBytes)
|
||||||
} else if strings.Contains(req.URL.String(), ".well-known/webfinger") {
|
} else if strings.Contains(reqURLString, ".well-known/webfinger") {
|
||||||
responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req)
|
responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req)
|
||||||
} else if strings.Contains(req.URL.String(), ".weird-webfinger-location/webfinger") {
|
} else if strings.Contains(reqURLString, ".weird-webfinger-location/webfinger") {
|
||||||
responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req)
|
responseCode, responseBytes, responseContentType, responseContentLength = WebfingerResponse(req)
|
||||||
} else if strings.Contains(req.URL.String(), ".well-known/host-meta") {
|
} else if strings.Contains(reqURLString, ".well-known/host-meta") {
|
||||||
responseCode, responseBytes, responseContentType, responseContentLength = HostMetaResponse(req)
|
responseCode, responseBytes, responseContentType, responseContentLength = HostMetaResponse(req)
|
||||||
} else if note, ok := mockHTTPClient.TestRemoteStatuses[req.URL.String()]; ok {
|
} else if note, ok := mockHTTPClient.TestRemoteStatuses[reqURLString]; ok {
|
||||||
// the request is for a note that we have stored
|
// the request is for a note that we have stored
|
||||||
noteI, err := streams.Serialize(note)
|
noteI, err := streams.Serialize(note)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -139,7 +142,7 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
responseBytes = noteJSON
|
responseBytes = noteJSON
|
||||||
responseContentType = applicationActivityJSON
|
responseContentType = applicationActivityJSON
|
||||||
responseContentLength = len(noteJSON)
|
responseContentLength = len(noteJSON)
|
||||||
} else if person, ok := mockHTTPClient.TestRemotePeople[req.URL.String()]; ok {
|
} else if person, ok := mockHTTPClient.TestRemotePeople[reqURLString]; ok {
|
||||||
// the request is for a person that we have stored
|
// the request is for a person that we have stored
|
||||||
personI, err := streams.Serialize(person)
|
personI, err := streams.Serialize(person)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -153,7 +156,7 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
responseBytes = personJSON
|
responseBytes = personJSON
|
||||||
responseContentType = applicationActivityJSON
|
responseContentType = applicationActivityJSON
|
||||||
responseContentLength = len(personJSON)
|
responseContentLength = len(personJSON)
|
||||||
} else if group, ok := mockHTTPClient.TestRemoteGroups[req.URL.String()]; ok {
|
} else if group, ok := mockHTTPClient.TestRemoteGroups[reqURLString]; ok {
|
||||||
// the request is for a person that we have stored
|
// the request is for a person that we have stored
|
||||||
groupI, err := streams.Serialize(group)
|
groupI, err := streams.Serialize(group)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -167,7 +170,7 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
responseBytes = groupJSON
|
responseBytes = groupJSON
|
||||||
responseContentType = applicationActivityJSON
|
responseContentType = applicationActivityJSON
|
||||||
responseContentLength = len(groupJSON)
|
responseContentLength = len(groupJSON)
|
||||||
} else if service, ok := mockHTTPClient.TestRemoteServices[req.URL.String()]; ok {
|
} else if service, ok := mockHTTPClient.TestRemoteServices[reqURLString]; ok {
|
||||||
serviceI, err := streams.Serialize(service)
|
serviceI, err := streams.Serialize(service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -180,7 +183,7 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
responseBytes = serviceJSON
|
responseBytes = serviceJSON
|
||||||
responseContentType = applicationActivityJSON
|
responseContentType = applicationActivityJSON
|
||||||
responseContentLength = len(serviceJSON)
|
responseContentLength = len(serviceJSON)
|
||||||
} else if emoji, ok := mockHTTPClient.TestRemoteEmojis[req.URL.String()]; ok {
|
} else if emoji, ok := mockHTTPClient.TestRemoteEmojis[reqURLString]; ok {
|
||||||
emojiI, err := streams.Serialize(emoji)
|
emojiI, err := streams.Serialize(emoji)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -193,16 +196,45 @@ func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relat
|
||||||
responseBytes = emojiJSON
|
responseBytes = emojiJSON
|
||||||
responseContentType = applicationActivityJSON
|
responseContentType = applicationActivityJSON
|
||||||
responseContentLength = len(emojiJSON)
|
responseContentLength = len(emojiJSON)
|
||||||
} else if attachment, ok := mockHTTPClient.TestRemoteAttachments[req.URL.String()]; ok {
|
} else if attachment, ok := mockHTTPClient.TestRemoteAttachments[reqURLString]; ok {
|
||||||
responseCode = http.StatusOK
|
responseCode = http.StatusOK
|
||||||
responseBytes = attachment.Data
|
responseBytes = attachment.Data
|
||||||
responseContentType = attachment.ContentType
|
responseContentType = attachment.ContentType
|
||||||
responseContentLength = len(attachment.Data)
|
responseContentLength = len(attachment.Data)
|
||||||
} else if _, ok := mockHTTPClient.TestTombstones[req.URL.String()]; ok {
|
} else if _, ok := mockHTTPClient.TestTombstones[reqURLString]; ok {
|
||||||
responseCode = http.StatusGone
|
responseCode = http.StatusGone
|
||||||
responseBytes = []byte{}
|
responseBytes = []byte{}
|
||||||
responseContentType = "text/html"
|
responseContentType = "text/html"
|
||||||
responseContentLength = 0
|
responseContentLength = 0
|
||||||
|
} else {
|
||||||
|
for _, person := range extraPeople {
|
||||||
|
// For any extra people, check if the
|
||||||
|
// request matches one of:
|
||||||
|
//
|
||||||
|
// - Public key URI
|
||||||
|
// - ActivityPub URI/id
|
||||||
|
// - Web URL.
|
||||||
|
//
|
||||||
|
// Since this is a test environment,
|
||||||
|
// just assume all these values have
|
||||||
|
// been properly set.
|
||||||
|
if reqURLString == person.GetW3IDSecurityV1PublicKey().At(0).Get().GetJSONLDId().GetIRI().String() ||
|
||||||
|
reqURLString == person.GetJSONLDId().GetIRI().String() ||
|
||||||
|
reqURLString == person.GetActivityStreamsUrl().At(0).GetIRI().String() {
|
||||||
|
personI, err := streams.Serialize(person)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
personJSON, err := json.Marshal(personI)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
responseCode = http.StatusOK
|
||||||
|
responseBytes = personJSON
|
||||||
|
responseContentType = applicationActivityJSON
|
||||||
|
responseContentLength = len(personJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf(nil, "returning response %s", string(responseBytes))
|
log.Debugf(nil, "returning response %s", string(responseBytes))
|
||||||
|
|
61
web/source/settings/admin/actions/keys/expireremote.jsx
Normal file
61
web/source/settings/admin/actions/keys/expireremote.jsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
|
||||||
|
const query = require("../../../lib/query");
|
||||||
|
|
||||||
|
const { useTextInput } = require("../../../lib/form");
|
||||||
|
const { TextInput } = require("../../../components/form/inputs");
|
||||||
|
|
||||||
|
const MutationButton = require("../../../components/form/mutation-button");
|
||||||
|
|
||||||
|
module.exports = function ExpireRemote({}) {
|
||||||
|
const domainField = useTextInput("domain");
|
||||||
|
|
||||||
|
const [expire, expireResult] = query.useInstanceKeysExpireMutation();
|
||||||
|
|
||||||
|
function submitExpire(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
expire(domainField.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={submitExpire}>
|
||||||
|
<h2>Expire remote instance keys</h2>
|
||||||
|
<p>
|
||||||
|
Mark all public keys from the given remote instance as expired.<br/><br/>
|
||||||
|
This is useful in cases where the remote domain has had to rotate their keys for whatever
|
||||||
|
reason (security issue, data leak, routine safety procedure, etc), and your instance can no
|
||||||
|
longer communicate with theirs properly using cached keys. A key marked as expired in this way
|
||||||
|
will be lazily refetched next time a request is made to your instance signed by the owner of that
|
||||||
|
key.
|
||||||
|
</p>
|
||||||
|
<TextInput
|
||||||
|
field={domainField}
|
||||||
|
label="Domain"
|
||||||
|
type="string"
|
||||||
|
placeholder="example.org"
|
||||||
|
/>
|
||||||
|
<MutationButton label="Expire keys" result={expireResult} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
32
web/source/settings/admin/actions/keys/index.jsx
Normal file
32
web/source/settings/admin/actions/keys/index.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const ExpireRemote = require("./expireremote");
|
||||||
|
|
||||||
|
module.exports = function Keys() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Key Actions</h1>
|
||||||
|
<ExpireRemote />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -21,42 +21,39 @@
|
||||||
|
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
|
|
||||||
const query = require("../lib/query");
|
const query = require("../../../lib/query");
|
||||||
|
|
||||||
const { useTextInput } = require("../lib/form");
|
const { useTextInput } = require("../../../lib/form");
|
||||||
const { TextInput } = require("../components/form/inputs");
|
const { TextInput } = require("../../../components/form/inputs");
|
||||||
|
|
||||||
const MutationButton = require("../components/form/mutation-button");
|
const MutationButton = require("../../../components/form/mutation-button");
|
||||||
|
|
||||||
module.exports = function AdminActionPanel() {
|
module.exports = function Cleanup({}) {
|
||||||
const daysField = useTextInput("days", { defaultValue: 30 });
|
const daysField = useTextInput("days", { defaultValue: 30 });
|
||||||
|
|
||||||
const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation();
|
const [mediaCleanup, mediaCleanupResult] = query.useMediaCleanupMutation();
|
||||||
|
|
||||||
function submitMediaCleanup(e) {
|
function submitCleanup(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
mediaCleanup(daysField.value);
|
mediaCleanup(daysField.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<form onSubmit={submitCleanup}>
|
||||||
<h1>Admin Actions</h1>
|
<h2>Cleanup</h2>
|
||||||
<form onSubmit={submitMediaCleanup}>
|
<p>
|
||||||
<h2>Media cleanup</h2>
|
|
||||||
<p>
|
|
||||||
Clean up remote media older than the specified number of days.
|
Clean up remote media older than the specified number of days.
|
||||||
If the remote instance is still online they will be refetched when needed.
|
If the remote instance is still online they will be refetched when needed.
|
||||||
Also cleans up unused headers and avatars from the media cache.
|
Also cleans up unused headers and avatars from the media cache.
|
||||||
</p>
|
</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
field={daysField}
|
field={daysField}
|
||||||
label="Days"
|
label="Days"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
/>
|
/>
|
||||||
<MutationButton label="Remove old media" result={mediaCleanupResult} />
|
<MutationButton label="Remove old media" result={mediaCleanupResult} />
|
||||||
</form>
|
</form>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
32
web/source/settings/admin/actions/media/index.jsx
Normal file
32
web/source/settings/admin/actions/media/index.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const React = require("react");
|
||||||
|
const Cleanup = require("./cleanup");
|
||||||
|
|
||||||
|
module.exports = function Media() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Media Actions</h1>
|
||||||
|
<Cleanup />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -55,7 +55,10 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
||||||
defaultUrl: "/settings/admin/settings",
|
defaultUrl: "/settings/admin/settings",
|
||||||
permissions: ["admin"]
|
permissions: ["admin"]
|
||||||
}, [
|
}, [
|
||||||
Item("Actions", { icon: "fa-bolt" }, require("./admin/actions")),
|
Menu("Actions", { icon: "fa-bolt" }, [
|
||||||
|
Item("Media", { icon: "fa-photo" }, require("./admin/actions/media")),
|
||||||
|
Item("Keys", { icon: "fa-key-modern" }, require("./admin/actions/keys")),
|
||||||
|
]),
|
||||||
Menu("Custom Emoji", { icon: "fa-smile-o" }, [
|
Menu("Custom Emoji", { icon: "fa-smile-o" }, [
|
||||||
Item("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),
|
Item("Local", { icon: "fa-home", wildcard: true }, require("./admin/emoji/local")),
|
||||||
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
|
Item("Remote", { icon: "fa-cloud" }, require("./admin/emoji/remote"))
|
||||||
|
@ -63,7 +66,7 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
|
||||||
Menu("Settings", { icon: "fa-sliders" }, [
|
Menu("Settings", { icon: "fa-sliders" }, [
|
||||||
Item("Settings", { icon: "fa-sliders", url: "" }, require("./admin/settings")),
|
Item("Settings", { icon: "fa-sliders", url: "" }, require("./admin/settings")),
|
||||||
Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, require("./admin/settings/rules"))
|
Item("Rules", { icon: "fa-dot-circle-o", wildcard: true }, require("./admin/settings/rules"))
|
||||||
])
|
]),
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,15 @@ const endpoints = (build) => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
instanceKeysExpire: build.mutation({
|
||||||
|
query: (domain) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/admin/domain_keys_expire`,
|
||||||
|
params: {
|
||||||
|
domain: domain
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
instanceBlocks: build.query({
|
instanceBlocks: build.query({
|
||||||
query: () => ({
|
query: () => ({
|
||||||
url: `/api/v1/admin/domain_blocks`
|
url: `/api/v1/admin/domain_blocks`
|
||||||
|
|
Loading…
Reference in a new issue