mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 14:52:26 -05:00
[feature] request blocking by http headers (#2409)
This commit is contained in:
parent
07bd848028
commit
8ebb7775a3
36 changed files with 2561 additions and 81 deletions
215
internal/processing/admin/headerfilter.go
Normal file
215
internal/processing/admin/headerfilter.go
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
// 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"
|
||||
"errors"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/headerfilter"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// GetAllowHeaderFilter fetches allow HTTP header filter with provided ID from the database.
|
||||
func (p *Processor) GetAllowHeaderFilter(ctx context.Context, id string) (*apimodel.HeaderFilter, gtserror.WithCode) {
|
||||
return p.getHeaderFilter(ctx, id, p.state.DB.GetAllowHeaderFilter)
|
||||
}
|
||||
|
||||
// GetBlockHeaderFilter fetches block HTTP header filter with provided ID from the database.
|
||||
func (p *Processor) GetBlockHeaderFilter(ctx context.Context, id string) (*apimodel.HeaderFilter, gtserror.WithCode) {
|
||||
return p.getHeaderFilter(ctx, id, p.state.DB.GetBlockHeaderFilter)
|
||||
}
|
||||
|
||||
// GetAllowHeaderFilters fetches all allow HTTP header filters stored in the database.
|
||||
func (p *Processor) GetAllowHeaderFilters(ctx context.Context) ([]*apimodel.HeaderFilter, gtserror.WithCode) {
|
||||
return p.getHeaderFilters(ctx, p.state.DB.GetAllowHeaderFilters)
|
||||
}
|
||||
|
||||
// GetBlockHeaderFilters fetches all block HTTP header filters stored in the database.
|
||||
func (p *Processor) GetBlockHeaderFilters(ctx context.Context) ([]*apimodel.HeaderFilter, gtserror.WithCode) {
|
||||
return p.getHeaderFilters(ctx, p.state.DB.GetBlockHeaderFilters)
|
||||
}
|
||||
|
||||
// CreateAllowHeaderFilter inserts the incoming allow HTTP header filter into the database, marking as authored by provided admin account.
|
||||
func (p *Processor) CreateAllowHeaderFilter(ctx context.Context, admin *gtsmodel.Account, request *apimodel.HeaderFilterRequest) (*apimodel.HeaderFilter, gtserror.WithCode) {
|
||||
return p.createHeaderFilter(ctx, admin, request, p.state.DB.PutAllowHeaderFilter)
|
||||
}
|
||||
|
||||
// CreateBlockHeaderFilter inserts the incoming block HTTP header filter into the database, marking as authored by provided admin account.
|
||||
func (p *Processor) CreateBlockHeaderFilter(ctx context.Context, admin *gtsmodel.Account, request *apimodel.HeaderFilterRequest) (*apimodel.HeaderFilter, gtserror.WithCode) {
|
||||
return p.createHeaderFilter(ctx, admin, request, p.state.DB.PutBlockHeaderFilter)
|
||||
}
|
||||
|
||||
// DeleteAllowHeaderFilter deletes the allowing HTTP header filter with provided ID from the database.
|
||||
func (p *Processor) DeleteAllowHeaderFilter(ctx context.Context, id string) gtserror.WithCode {
|
||||
return p.deleteHeaderFilter(ctx, id, p.state.DB.DeleteAllowHeaderFilter)
|
||||
}
|
||||
|
||||
// DeleteBlockHeaderFilter deletes the blocking HTTP header filter with provided ID from the database.
|
||||
func (p *Processor) DeleteBlockHeaderFilter(ctx context.Context, id string) gtserror.WithCode {
|
||||
return p.deleteHeaderFilter(ctx, id, p.state.DB.DeleteBlockHeaderFilter)
|
||||
}
|
||||
|
||||
// getHeaderFilter fetches an HTTP header filter with
|
||||
// provided ID, using given get function, converting the
|
||||
// resulting filter to returnable frontend API model.
|
||||
func (p *Processor) getHeaderFilter(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
get func(context.Context, string) (*gtsmodel.HeaderFilter, error),
|
||||
) (
|
||||
*apimodel.HeaderFilter,
|
||||
gtserror.WithCode,
|
||||
) {
|
||||
// Select filter by ID from db.
|
||||
filter, err := get(ctx, id)
|
||||
|
||||
switch {
|
||||
// Successfully found.
|
||||
case err == nil:
|
||||
return toAPIHeaderFilter(filter), nil
|
||||
|
||||
// Filter does not exist with ID.
|
||||
case errors.Is(err, db.ErrNoEntries):
|
||||
const text = "filter not found"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text), text)
|
||||
|
||||
// Any other error type.
|
||||
default:
|
||||
err := gtserror.Newf("error selecting from database: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// getHeaderFilters fetches all HTTP header filters
|
||||
// using given get function, converting the resulting
|
||||
// filters to returnable frontend API models.
|
||||
func (p *Processor) getHeaderFilters(
|
||||
ctx context.Context,
|
||||
get func(context.Context) ([]*gtsmodel.HeaderFilter, error),
|
||||
) (
|
||||
[]*apimodel.HeaderFilter,
|
||||
gtserror.WithCode,
|
||||
) {
|
||||
// Select all filters from DB.
|
||||
filters, err := get(ctx)
|
||||
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
// Only handle errors other than not-found types.
|
||||
err := gtserror.Newf("error selecting from database: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Convert passed header filters to apimodel filters.
|
||||
apiFilters := make([]*apimodel.HeaderFilter, len(filters))
|
||||
for i := range filters {
|
||||
apiFilters[i] = toAPIHeaderFilter(filters[i])
|
||||
}
|
||||
|
||||
return apiFilters, nil
|
||||
}
|
||||
|
||||
// createHeaderFilter inserts the given HTTP header
|
||||
// filter into database, marking as authored by the
|
||||
// provided admin, using the given insert function.
|
||||
func (p *Processor) createHeaderFilter(
|
||||
ctx context.Context,
|
||||
admin *gtsmodel.Account,
|
||||
request *apimodel.HeaderFilterRequest,
|
||||
insert func(context.Context, *gtsmodel.HeaderFilter) error,
|
||||
) (
|
||||
*apimodel.HeaderFilter,
|
||||
gtserror.WithCode,
|
||||
) {
|
||||
// Convert header key to canonical mime header format.
|
||||
request.Header = textproto.CanonicalMIMEHeaderKey(request.Header)
|
||||
|
||||
// Validate incoming header filter.
|
||||
if errWithCode := validateHeaderFilter(
|
||||
request.Header,
|
||||
request.Regex,
|
||||
); errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
// Create new database model with ID.
|
||||
var filter gtsmodel.HeaderFilter
|
||||
filter.ID = id.NewULID()
|
||||
filter.Header = request.Header
|
||||
filter.Regex = request.Regex
|
||||
filter.AuthorID = admin.ID
|
||||
filter.Author = admin
|
||||
|
||||
// Insert new header filter into the database.
|
||||
if err := insert(ctx, &filter); err != nil {
|
||||
err := gtserror.Newf("error inserting into database: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Finally return API model response.
|
||||
return toAPIHeaderFilter(&filter), nil
|
||||
}
|
||||
|
||||
// deleteHeaderFilter deletes the HTTP header filter
|
||||
// with provided ID, using the given delete function.
|
||||
func (p *Processor) deleteHeaderFilter(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
delete func(context.Context, string) error,
|
||||
) gtserror.WithCode {
|
||||
if err := delete(ctx, id); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("error deleting from database: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toAPIFilter performs a simple conversion of database model HeaderFilter to API model.
|
||||
func toAPIHeaderFilter(filter *gtsmodel.HeaderFilter) *apimodel.HeaderFilter {
|
||||
return &apimodel.HeaderFilter{
|
||||
ID: filter.ID,
|
||||
Header: filter.Header,
|
||||
Regex: filter.Regex,
|
||||
CreatedBy: filter.AuthorID,
|
||||
CreatedAt: util.FormatISO8601(filter.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
// validateHeaderFilter validates incoming filter's header key, and regular expression.
|
||||
func validateHeaderFilter(header, regex string) gtserror.WithCode {
|
||||
// Check header validity (within our own bound checks).
|
||||
if header == "" || len(header) > headerfilter.MaxHeaderValue {
|
||||
const text = "invalid request header key (empty or too long)"
|
||||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Ensure this is compilable regex.
|
||||
_, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue