diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/client/instance/domainperms.go | 172 | ||||
| -rw-r--r-- | internal/api/client/instance/instance.go | 7 | ||||
| -rw-r--r-- | internal/api/client/instance/instancepeersget.go | 110 | ||||
| -rw-r--r-- | internal/api/client/instance/instancepeersget_test.go | 81 | ||||
| -rw-r--r-- | internal/api/model/domain.go | 7 | ||||
| -rw-r--r-- | internal/config/config.go | 11 | ||||
| -rw-r--r-- | internal/config/defaults.go | 4 | ||||
| -rw-r--r-- | internal/config/helpers.gen.go | 142 | ||||
| -rw-r--r-- | internal/processing/instance.go | 145 | ||||
| -rw-r--r-- | internal/web/about.go | 3 | ||||
| -rw-r--r-- | internal/web/domain-blocklist.go | 75 | ||||
| -rw-r--r-- | internal/web/domainperms.go | 136 | ||||
| -rw-r--r-- | internal/web/web.go | 3 |
13 files changed, 694 insertions, 202 deletions
diff --git a/internal/api/client/instance/domainperms.go b/internal/api/client/instance/domainperms.go new file mode 100644 index 000000000..6503388a5 --- /dev/null +++ b/internal/api/client/instance/domainperms.go @@ -0,0 +1,172 @@ +// 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 instance + +import ( + "errors" + "net/http" + + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" + "code.superseriousbusiness.org/gotosocial/internal/config" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "github.com/gin-gonic/gin" +) + +// InstanceDomainBlocksGETHandler swagger:operation GET /api/v1/instance/domain_blocks instanceDomainBlocksGet +// +// List blocked domains. +// +// OAuth token may need to be provided depending on setting `instance-expose-blocklist`. +// +// --- +// tags: +// - instance +// +// produces: +// - application/json +// +// security: +// - OAuth2 Bearer: [] +// +// responses: +// '200': +// description: List of blocked domains. +// schema: +// type: array +// items: +// "$ref": "#/definitions/domain" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) InstanceDomainBlocksGETHandler(c *gin.Context) { + authed, errWithCode := apiutil.TokenAuth(c, + false, false, false, false, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, 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 + } + + if (authed.Account == nil || authed.User == nil) && !config.GetInstanceExposeBlocklist() { + const errText = "domain blocks endpoint requires an authenticated account/user" + errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + data, errWithCode := m.processor.InstancePeersGet( + c.Request.Context(), + true, // Include blocked. + false, // Don't include allowed. + false, // Don't include open. + false, // Don't flatten. + true, // Include severity. + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, data) +} + +// InstanceDomainAllowsGETHandler swagger:operation GET /api/v1/instance/domain_allows instanceDomainAllowsGet +// +// List explicitly allowed domains. +// +// OAuth token may need to be provided depending on setting `instance-expose-allowlist`. +// +// --- +// tags: +// - instance +// +// produces: +// - application/json +// +// security: +// - OAuth2 Bearer: [] +// +// responses: +// '200': +// description: List of explicitly allowed domains. +// schema: +// type: array +// items: +// "$ref": "#/definitions/domain" +// '400': +// description: bad request +// '401': +// description: unauthorized +// '403': +// description: forbidden +// '404': +// description: not found +// '406': +// description: not acceptable +// '500': +// description: internal server error +func (m *Module) InstanceDomainAllowsGETHandler(c *gin.Context) { + authed, errWithCode := apiutil.TokenAuth(c, + false, false, false, false, + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, 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 + } + + if (authed.Account == nil || authed.User == nil) && !config.GetInstanceExposeAllowlist() { + const errText = "domain allows endpoint requires an authenticated account/user" + errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + data, errWithCode := m.processor.InstancePeersGet( + c.Request.Context(), + false, // Don't include blocked. + true, // Include allowed. + false, // Don't include open. + false, // Don't flatten. + false, // Don't include severity. + ) + if errWithCode != nil { + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + apiutil.JSON(c, http.StatusOK, data) +} diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go index cd6c438c8..0e06941cc 100644 --- a/internal/api/client/instance/instance.go +++ b/internal/api/client/instance/instance.go @@ -29,7 +29,10 @@ const ( InstanceInformationPathV2 = "/v2/instance" InstancePeersPath = InstanceInformationPathV1 + "/peers" InstanceRulesPath = InstanceInformationPathV1 + "/rules" + InstanceBlocklistPath = InstanceInformationPathV1 + "/domain_blocks" + InstanceAllowlistPath = InstanceInformationPathV1 + "/domain_allows" PeersFilterKey = "filter" // PeersFilterKey is used to provide filters to /api/v1/instance/peers + PeersFlatKey = "flat" // PeersFlatKey is used to set "flat=true" in /api/v1/instance/peers ) type Module struct { @@ -45,9 +48,9 @@ func New(processor *processing.Processor) *Module { func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) { attachHandler(http.MethodGet, InstanceInformationPathV1, m.InstanceInformationGETHandlerV1) attachHandler(http.MethodGet, InstanceInformationPathV2, m.InstanceInformationGETHandlerV2) - attachHandler(http.MethodPatch, InstanceInformationPathV1, m.InstanceUpdatePATCHHandler) attachHandler(http.MethodGet, InstancePeersPath, m.InstancePeersGETHandler) - attachHandler(http.MethodGet, InstanceRulesPath, m.InstanceRulesGETHandler) + attachHandler(http.MethodGet, InstanceBlocklistPath, m.InstanceDomainBlocksGETHandler) + attachHandler(http.MethodGet, InstanceAllowlistPath, m.InstanceDomainAllowsGETHandler) } diff --git a/internal/api/client/instance/instancepeersget.go b/internal/api/client/instance/instancepeersget.go index 7afeb7104..d9f7610b7 100644 --- a/internal/api/client/instance/instancepeersget.go +++ b/internal/api/client/instance/instancepeersget.go @@ -18,8 +18,10 @@ package instance import ( + "errors" "fmt" "net/http" + "strconv" "strings" apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" @@ -31,6 +33,8 @@ import ( // InstancePeersGETHandler swagger:operation GET /api/v1/instance/peers instancePeersGet // +// List peer domains. +// // --- // tags: // - instance @@ -44,19 +48,32 @@ import ( // type: string // description: |- // Comma-separated list of filters to apply to results. Recognized filters are: -// - `open` -- include peers that are not suspended or silenced -// - `suspended` -- include peers that have been suspended. +// - `open` -- include known domains that are not in the domain blocklist +// - `allowed` -- include domains that are in the domain allowlist +// - `blocked` -- include domains that are in the domain blocklist +// - `suspended` -- DEPRECATED! Use `blocked` instead. Same as `blocked`: include domains that are in the domain blocklist; +// +// If filter is `open`, only domains that aren't in the blocklist will be shown. // -// If filter is `open`, only instances that haven't been suspended or silenced will be returned. +// If filter is `blocked`, only domains that *are* in the blocklist will be shown. // -// If filter is `suspended`, only suspended instances will be shown. +// If filter is `allowed`, only domains that are in the allowlist will be shown. // -// If filter is `open,suspended`, then all known instances will be returned. +// If filter is `open,blocked`, then blocked domains and known domains not on the blocklist will be shown. +// +// If filter is `open,allowed`, then allowed domains and known domains not on the blocklist will be shown. // // If filter is an empty string or not set, then `open` will be assumed as the default. // in: query // required: false -// default: "open" +// default: flat +// - +// name: flat +// type: boolean +// description: If true, a "flat" array of strings will be returned corresponding to just domain names. +// in: query +// required: false +// default: false // // security: // - OAuth2 Bearer: [] @@ -67,12 +84,10 @@ import ( // If no filter parameter is provided, or filter is empty, then a legacy, // Mastodon-API compatible response will be returned. This will consist of // just a 'flat' array of strings like `["example.com", "example.org"]`, -// which corresponds to domains this instance peers with. -// -// -// If a filter parameter is provided, then an array of objects with at least -// a `domain` key set on each object will be returned. +// which corresponds to setting a filter of `open` and flat=true. // +// If a filter parameter is provided and flat is not true, then an array +// of objects with at least a `domain` key set on each object will be returned. // // Domains that are silenced or suspended will also have a key // `suspended_at` or `silenced_at` that contains an iso8601 date string. @@ -81,7 +96,6 @@ import ( // will have some letters replaced by `*` to make it more difficult for // bad actors to target instances with harassment. // -// // Whether a flat response or a more detailed response is returned, domains // will be sorted alphabetically by hostname. // schema: @@ -116,45 +130,85 @@ func (m *Module) InstancePeersGETHandler(c *gin.Context) { return } - var includeSuspended bool - var includeOpen bool - var flat bool + var ( + includeBlocked bool + includeAllowed bool + includeOpen bool + flatten bool + ) + if filterParam := c.Query(PeersFilterKey); filterParam != "" { filters := strings.Split(filterParam, ",") for _, f := range filters { trimmed := strings.TrimSpace(f) switch { - case strings.EqualFold(trimmed, "suspended"): - includeSuspended = true + case strings.EqualFold(trimmed, "blocked") || strings.EqualFold(trimmed, "suspended"): + includeBlocked = true + case strings.EqualFold(trimmed, "allowed"): + includeAllowed = true case strings.EqualFold(trimmed, "open"): includeOpen = true default: - err := fmt.Errorf("filter %s not recognized; accepted values are 'open', 'suspended'", trimmed) + err := fmt.Errorf("filter %s not recognized; accepted values are 'open', 'blocked', 'allowed', and 'suspended' (deprecated)", trimmed) apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) return } } } else { - // default is to only include open domains, and present + // Default is to only include open domains, and present // them in a 'flat' manner (just an array of strings), - // to maintain compatibility with mastodon API + // to maintain compatibility with the Mastodon API. includeOpen = true - flat = true + flatten = true } - if includeOpen && !config.GetInstanceExposePeers() && isUnauthenticated { - err := fmt.Errorf("peers open query requires an authenticated account/user") - apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + if includeBlocked && isUnauthenticated && !config.GetInstanceExposeBlocklist() { + const errText = "peers blocked query requires an authenticated account/user" + errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - if includeSuspended && !config.GetInstanceExposeSuspended() && isUnauthenticated { - err := fmt.Errorf("peers suspended query requires an authenticated account/user") - apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1) + if includeAllowed && isUnauthenticated && !config.GetInstanceExposeAllowlist() { + const errText = "peers allowed query requires an authenticated account/user" + errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - data, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), includeSuspended, includeOpen, flat) + if includeOpen && isUnauthenticated && !config.GetInstanceExposePeers() { + const errText = "peers open query requires an authenticated account/user" + errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if includeBlocked && includeAllowed { + const errText = "cannot include blocked + allowed filters at the same time" + errWithCode := gtserror.NewErrorBadRequest(errors.New(errText), errText) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + if flatStr := c.Query(PeersFlatKey); flatStr != "" { + var err error + flatten, err = strconv.ParseBool(flatStr) + if err != nil { + err := fmt.Errorf("error parsing 'flat' key as boolean: %w", err) + errWithCode := gtserror.NewErrorBadRequest(err, err.Error()) + apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + } + + data, errWithCode := m.processor.InstancePeersGet( + c.Request.Context(), + includeBlocked, + includeAllowed, + includeOpen, + flatten, + false, // Don't include severity. + ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/instance/instancepeersget_test.go b/internal/api/client/instance/instancepeersget_test.go index a18e30875..3c7f1f665 100644 --- a/internal/api/client/instance/instancepeersget_test.go +++ b/internal/api/client/instance/instancepeersget_test.go @@ -136,13 +136,14 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspended() { { "domain": "replyguys.com", "suspended_at": "2020-05-13T13:29:12.000Z", - "comment": "reply-guying to tech posts" + "comment": "reply-guying to tech posts", + "severity": "suspend" } ]`, dst.String()) } func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedUnauthorized() { - config.SetInstanceExposeSuspended(false) + config.SetInstanceExposeBlocklist(false) recorder := httptest.NewRecorder() baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) @@ -159,11 +160,11 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedUnautho b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"error":"Unauthorized: peers suspended query requires an authenticated account/user"}`, string(b)) + suite.Equal(`{"error":"Unauthorized: peers blocked query requires an authenticated account/user"}`, string(b)) } func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedAuthorized() { - config.SetInstanceExposeSuspended(false) + config.SetInstanceExposeBlocklist(false) recorder := httptest.NewRecorder() baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) @@ -186,7 +187,8 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetOnlySuspendedAuthori { "domain": "replyguys.com", "suspended_at": "2020-05-13T13:29:12.000Z", - "comment": "reply-guying to tech posts" + "comment": "reply-guying to tech posts", + "severity": "suspend" } ]`, dst.String()) } @@ -219,11 +221,33 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAll() { { "domain": "replyguys.com", "suspended_at": "2020-05-13T13:29:12.000Z", - "comment": "reply-guying to tech posts" + "comment": "reply-guying to tech posts", + "severity": "suspend" } ]`, dst.String()) } +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllowed() { + recorder := httptest.NewRecorder() + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=allowed", baseURI, instance.InstancePeersPath) + ctx := suite.newContext(recorder, http.MethodGet, requestURI, nil, "", false) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + dst := new(bytes.Buffer) + err = json.Indent(dst, b, "", " ") + suite.NoError(err) + suite.Equal(`[]`, dst.String()) +} + func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated() { err := suite.db.Put(context.Background(), >smodel.DomainBlock{ ID: "01G633XTNK51GBADQZFZQDP6WR", @@ -263,16 +287,55 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscated() { "domain": "o*g.*u**.t**.*or*t.*r**ev**", "suspended_at": "2021-06-09T10:34:55.000Z", - "comment": "just absolutely the worst, wowza" + "comment": "just absolutely the worst, wowza", + "severity": "suspend" }, { "domain": "replyguys.com", "suspended_at": "2020-05-13T13:29:12.000Z", - "comment": "reply-guying to tech posts" + "comment": "reply-guying to tech posts", + "severity": "suspend" } ]`, dst.String()) } +func (suite *InstancePeersGetTestSuite) TestInstancePeersGetAllWithObfuscatedFlat() { + err := suite.db.Put(context.Background(), >smodel.DomainBlock{ + ID: "01G633XTNK51GBADQZFZQDP6WR", + CreatedAt: testrig.TimeMustParse("2021-06-09T12:34:55+02:00"), + UpdatedAt: testrig.TimeMustParse("2021-06-09T12:34:55+02:00"), + Domain: "omg.just.the.worst.org.ever", + CreatedByAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", + PublicComment: "just absolutely the worst, wowza", + Obfuscate: util.Ptr(true), + }) + suite.NoError(err) + + recorder := httptest.NewRecorder() + baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) + requestURI := fmt.Sprintf("%s/%s?filter=suspended,open&flat=true", baseURI, instance.InstancePeersPath) + ctx := suite.newContext(recorder, http.MethodGet, requestURI, nil, "", false) + + suite.instanceModule.InstancePeersGETHandler(ctx) + + suite.Equal(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + + b, err := io.ReadAll(result.Body) + suite.NoError(err) + dst := new(bytes.Buffer) + err = json.Indent(dst, b, "", " ") + suite.NoError(err) + suite.Equal(`[ + "example.org", + "fossbros-anonymous.io", + "o*g.*u**.t**.*or*t.*r**ev**", + "replyguys.com" +]`, dst.String()) +} + func (suite *InstancePeersGetTestSuite) TestInstancePeersGetFunkyParams() { recorder := httptest.NewRecorder() baseURI := fmt.Sprintf("%s://%s", config.GetProtocol(), config.GetHost()) @@ -289,7 +352,7 @@ func (suite *InstancePeersGetTestSuite) TestInstancePeersGetFunkyParams() { b, err := io.ReadAll(result.Body) suite.NoError(err) - suite.Equal(`{"error":"Bad Request: filter aaaaaaaaaaaaaaaaa not recognized; accepted values are 'open', 'suspended'"}`, string(b)) + suite.Equal(`{"error":"Bad Request: filter aaaaaaaaaaaaaaaaa not recognized; accepted values are 'open', 'blocked', 'allowed', and 'suspended' (deprecated)"}`, string(b)) } func TestInstancePeersGetTestSuite(t *testing.T) { diff --git a/internal/api/model/domain.go b/internal/api/model/domain.go index 8d94321d0..b793d77b0 100644 --- a/internal/api/model/domain.go +++ b/internal/api/model/domain.go @@ -32,14 +32,17 @@ type Domain struct { // Time at which this domain was silenced. Key will not be present on open domains. // example: 2021-07-30T09:20:25+00:00 SilencedAt string `json:"silenced_at,omitempty"` - // If the domain is blocked, what's the publicly-stated reason for the block. + // If the domain is blocked or allowed, what's the publicly-stated reason (if any). // Alternative to `public_comment` to be used when serializing/deserializing via /api/v1/instance. // example: they smell Comment *string `form:"comment" json:"comment,omitempty"` - // If the domain is blocked, what's the publicly-stated reason for the block. + // If the domain is blocked or allowed, what's the publicly-stated reason (if any). // Alternative to `comment` to be used when serializing/deserializing NOT via /api/v1/instance. // example: they smell PublicComment *string `form:"public_comment" json:"public_comment,omitempty"` + // Severity of this entry. + // Only ever set for domain blocks, and if set, always="suspend". + Severity string `form:"severity" json:"severity,omitempty"` } // DomainPermission represents a permission applied to one domain (explicit block/allow). diff --git a/internal/config/config.go b/internal/config/config.go index dd03b1104..5b8da9703 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,7 +49,10 @@ func fieldtag(field, tag string) string { // // Please note that if you update this struct's fields or tags, you // will need to regenerate the global Getter/Setter helpers by running: -// `go run ./internal/config/gen/ -out ./internal/config/helpers.gen.go` +// `go run ./internal/config/gen/ -out ./internal/config/helpers.gen.go`. +// +// You will need to have gofumpt installed in order for this to work: +// https://github.com/mvdan/gofumpt. type Configuration struct { LogLevel string `name:"log-level" usage:"Log level to run at: [trace, debug, info, warn, fatal]"` LogTimestampFormat string `name:"log-timestamp-format" usage:"Format to use for the log timestamp, as supported by Go's time.Layout"` @@ -88,8 +91,10 @@ type Configuration struct { InstanceFederationMode string `name:"instance-federation-mode" usage:"Set instance federation mode."` InstanceFederationSpamFilter bool `name:"instance-federation-spam-filter" usage:"Enable basic spam filter heuristics for messages coming from other instances, and drop messages identified as spam"` InstanceExposePeers bool `name:"instance-expose-peers" usage:"Allow unauthenticated users to query /api/v1/instance/peers?filter=open"` - InstanceExposeSuspended bool `name:"instance-expose-suspended" usage:"Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended"` - InstanceExposeSuspendedWeb bool `name:"instance-expose-suspended-web" usage:"Expose list of suspended instances as webpage on /about/suspended"` + InstanceExposeBlocklist bool `name:"instance-expose-blocklist" usage:"Expose list of blocked domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=blocked and /api/v1/instance/domain_blocks"` + InstanceExposeBlocklistWeb bool `name:"instance-expose-blocklist-web" usage:"Expose list of explicitly blocked domains as webpage on /about/domain_blocks"` + InstanceExposeAllowlist bool `name:"instance-expose-allowlist" usage:"Expose list of allowed domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=allowed and /api/v1/instance/domain_allows"` + InstanceExposeAllowlistWeb bool `name:"instance-expose-allowlist-web" usage:"Expose list of explicitly allowed domains as webpage on /about/domain_allows"` InstanceExposePublicTimeline bool `name:"instance-expose-public-timeline" usage:"Allow unauthenticated users to query /api/v1/timelines/public"` InstanceDeliverToSharedInboxes bool `name:"instance-deliver-to-shared-inboxes" usage:"Deliver federated messages to shared inboxes, if they're available."` InstanceInjectMastodonVersion bool `name:"instance-inject-mastodon-version" usage:"This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection"` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 8ed6ed0ea..8372e4b00 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -61,8 +61,8 @@ var Defaults = Configuration{ InstanceFederationMode: InstanceFederationModeDefault, InstanceFederationSpamFilter: false, InstanceExposePeers: false, - InstanceExposeSuspended: false, - InstanceExposeSuspendedWeb: false, + InstanceExposeBlocklist: false, + InstanceExposeBlocklistWeb: false, InstanceDeliverToSharedInboxes: true, InstanceLanguages: make(language.Languages, 0), InstanceSubscriptionsProcessFrom: "23:00", // 11pm, diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index a41c95861..269aa7abc 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -63,8 +63,10 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) { flags.String("instance-federation-mode", cfg.InstanceFederationMode, "Set instance federation mode.") flags.Bool("instance-federation-spam-filter", cfg.InstanceFederationSpamFilter, "Enable basic spam filter heuristics for messages coming from other instances, and drop messages identified as spam") flags.Bool("instance-expose-peers", cfg.InstanceExposePeers, "Allow unauthenticated users to query /api/v1/instance/peers?filter=open") - flags.Bool("instance-expose-suspended", cfg.InstanceExposeSuspended, "Expose suspended instances via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=suspended") - flags.Bool("instance-expose-suspended-web", cfg.InstanceExposeSuspendedWeb, "Expose list of suspended instances as webpage on /about/suspended") + flags.Bool("instance-expose-blocklist", cfg.InstanceExposeBlocklist, "Expose list of blocked domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=blocked and /api/v1/instance/domain_blocks") + flags.Bool("instance-expose-blocklist-web", cfg.InstanceExposeBlocklistWeb, "Expose list of explicitly blocked domains as webpage on /about/domain_blocks") + flags.Bool("instance-expose-allowlist", cfg.InstanceExposeAllowlist, "Expose list of allowed domains via web UI, and allow unauthenticated users to query /api/v1/instance/peers?filter=allowed and /api/v1/instance/domain_allows") + flags.Bool("instance-expose-allowlist-web", cfg.InstanceExposeAllowlistWeb, "Expose list of explicitly allowed domains as webpage on /about/domain_allows") flags.Bool("instance-expose-public-timeline", cfg.InstanceExposePublicTimeline, "Allow unauthenticated users to query /api/v1/timelines/public") flags.Bool("instance-deliver-to-shared-inboxes", cfg.InstanceDeliverToSharedInboxes, "Deliver federated messages to shared inboxes, if they're available.") flags.Bool("instance-inject-mastodon-version", cfg.InstanceInjectMastodonVersion, "This injects a Mastodon compatible version in /api/v1/instance to help Mastodon clients that use that version for feature detection") @@ -79,7 +81,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) { flags.Int("accounts-registration-backlog-limit", cfg.AccountsRegistrationBacklogLimit, "Limit how big the 'accounts pending approval' queue can grow before registration is closed. 0 or less = no limit.") flags.Bool("accounts-allow-custom-css", cfg.AccountsAllowCustomCSS, "Allow accounts to enable custom CSS for their profile pages and statuses.") flags.Int("accounts-custom-css-length", cfg.AccountsCustomCSSLength, "Maximum permitted length (characters) of custom CSS for accounts.") - flags.Int("accounts-max-profile-fields", cfg.AccountsMaxProfileFields, "Maximum amount of profile fields an account can have.") + flags.Int("accounts-max-profile-fields", cfg.AccountsMaxProfileFields, "Maximum number of profile fields allowed for each account.") flags.Int("media-description-min-chars", cfg.MediaDescriptionMinChars, "Min required chars for an image description") flags.Int("media-description-max-chars", cfg.MediaDescriptionMaxChars, "Max permitted chars for an image description") flags.Int("media-remote-cache-days", cfg.MediaRemoteCacheDays, "Number of days to locally cache media from remote instances. If set to 0, remote media will be kept indefinitely.") @@ -207,7 +209,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) { } func (cfg *Configuration) MarshalMap() map[string]any { - cfgmap := make(map[string]any, 182) + cfgmap := make(map[string]any, 184) cfgmap["log-level"] = cfg.LogLevel cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat cfgmap["log-db-queries"] = cfg.LogDbQueries @@ -242,8 +244,10 @@ func (cfg *Configuration) MarshalMap() map[string]any { cfgmap["instance-federation-mode"] = cfg.InstanceFederationMode cfgmap["instance-federation-spam-filter"] = cfg.InstanceFederationSpamFilter cfgmap["instance-expose-peers"] = cfg.InstanceExposePeers - cfgmap["instance-expose-suspended"] = cfg.InstanceExposeSuspended - cfgmap["instance-expose-suspended-web"] = cfg.InstanceExposeSuspendedWeb + cfgmap["instance-expose-blocklist"] = cfg.InstanceExposeBlocklist + cfgmap["instance-expose-blocklist-web"] = cfg.InstanceExposeBlocklistWeb + cfgmap["instance-expose-allowlist"] = cfg.InstanceExposeAllowlist + cfgmap["instance-expose-allowlist-web"] = cfg.InstanceExposeAllowlistWeb cfgmap["instance-expose-public-timeline"] = cfg.InstanceExposePublicTimeline cfgmap["instance-deliver-to-shared-inboxes"] = cfg.InstanceDeliverToSharedInboxes cfgmap["instance-inject-mastodon-version"] = cfg.InstanceInjectMastodonVersion @@ -674,19 +678,35 @@ func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error { } } - if ival, ok := cfgmap["instance-expose-suspended"]; ok { + if ival, ok := cfgmap["instance-expose-blocklist"]; ok { var err error - cfg.InstanceExposeSuspended, err = cast.ToBoolE(ival) + cfg.InstanceExposeBlocklist, err = cast.ToBoolE(ival) if err != nil { - return fmt.Errorf("error casting %#v -> bool for 'instance-expose-suspended': %w", ival, err) + return fmt.Errorf("error casting %#v -> bool for 'instance-expose-blocklist': %w", ival, err) } } - if ival, ok := cfgmap["instance-expose-suspended-web"]; ok { + if ival, ok := cfgmap["instance-expose-blocklist-web"]; ok { var err error - cfg.InstanceExposeSuspendedWeb, err = cast.ToBoolE(ival) + cfg.InstanceExposeBlocklistWeb, err = cast.ToBoolE(ival) if err != nil { - return fmt.Errorf("error casting %#v -> bool for 'instance-expose-suspended-web': %w", ival, err) + return fmt.Errorf("error casting %#v -> bool for 'instance-expose-blocklist-web': %w", ival, err) + } + } + + if ival, ok := cfgmap["instance-expose-allowlist"]; ok { + var err error + cfg.InstanceExposeAllowlist, err = cast.ToBoolE(ival) + if err != nil { + return fmt.Errorf("error casting %#v -> bool for 'instance-expose-allowlist': %w", ival, err) + } + } + + if ival, ok := cfgmap["instance-expose-allowlist-web"]; ok { + var err error + cfg.InstanceExposeAllowlistWeb, err = cast.ToBoolE(ival) + if err != nil { + return fmt.Errorf("error casting %#v -> bool for 'instance-expose-allowlist-web': %w", ival, err) } } @@ -2742,55 +2762,105 @@ func GetInstanceExposePeers() bool { return global.GetInstanceExposePeers() } // SetInstanceExposePeers safely sets the value for global configuration 'InstanceExposePeers' field func SetInstanceExposePeers(v bool) { global.SetInstanceExposePeers(v) } -// InstanceExposeSuspendedFlag returns the flag name for the 'InstanceExposeSuspended' field -func InstanceExposeSuspendedFlag() string { return "instance-expose-suspended" } +// InstanceExposeBlocklistFlag returns the flag name for the 'InstanceExposeBlocklist' field +func InstanceExposeBlocklistFlag() string { return "instance-expose-blocklist" } + +// GetInstanceExposeBlocklist safely fetches the Configuration value for state's 'InstanceExposeBlocklist' field +func (st *ConfigState) GetInstanceExposeBlocklist() (v bool) { + st.mutex.RLock() + v = st.config.InstanceExposeBlocklist + st.mutex.RUnlock() + return +} + +// SetInstanceExposeBlocklist safely sets the Configuration value for state's 'InstanceExposeBlocklist' field +func (st *ConfigState) SetInstanceExposeBlocklist(v bool) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.InstanceExposeBlocklist = v + st.reloadToViper() +} + +// GetInstanceExposeBlocklist safely fetches the value for global configuration 'InstanceExposeBlocklist' field +func GetInstanceExposeBlocklist() bool { return global.GetInstanceExposeBlocklist() } + +// SetInstanceExposeBlocklist safely sets the value for global configuration 'InstanceExposeBlocklist' field +func SetInstanceExposeBlocklist(v bool) { global.SetInstanceExposeBlocklist(v) } + +// InstanceExposeBlocklistWebFlag returns the flag name for the 'InstanceExposeBlocklistWeb' field +func InstanceExposeBlocklistWebFlag() string { return "instance-expose-blocklist-web" } + +// GetInstanceExposeBlocklistWeb safely fetches the Configuration value for state's 'InstanceExposeBlocklistWeb' field +func (st *ConfigState) GetInstanceExposeBlocklistWeb() (v bool) { + st.mutex.RLock() + v = st.config.InstanceExposeBlocklistWeb + st.mutex.RUnlock() + return +} + +// SetInstanceExposeBlocklistWeb safely sets the Configuration value for state's 'InstanceExposeBlocklistWeb' field +func (st *ConfigState) SetInstanceExposeBlocklistWeb(v bool) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.InstanceExposeBlocklistWeb = v + st.reloadToViper() +} + +// GetInstanceExposeBlocklistWeb safely fetches the value for global configuration 'InstanceExposeBlocklistWeb' field +func GetInstanceExposeBlocklistWeb() bool { return global.GetInstanceExposeBlocklistWeb() } + +// SetInstanceExposeBlocklistWeb safely sets the value for global configuration 'InstanceExposeBlocklistWeb' field +func SetInstanceExposeBlocklistWeb(v bool) { global.SetInstanceExposeBlocklistWeb(v) } + +// InstanceExposeAllowlistFlag returns the flag name for the 'InstanceExposeAllowlist' field +func InstanceExposeAllowlistFlag() string { return "instance-expose-allowlist" } -// GetInstanceExposeSuspended safely fetches the Configuration value for state's 'InstanceExposeSuspended' field -func (st *ConfigState) GetInstanceExposeSuspended() (v bool) { +// GetInstanceExposeAllowlist safely fetches the Configuration value for state's 'InstanceExposeAllowlist' field +func (st *ConfigState) GetInstanceExposeAllowlist() (v bool) { st.mutex.RLock() - v = st.config.InstanceExposeSuspended + v = st.config.InstanceExposeAllowlist st.mutex.RUnlock() return } -// SetInstanceExposeSuspended safely sets the Configuration value for state's 'InstanceExposeSuspended' field -func (st *ConfigState) SetInstanceExposeSuspended(v bool) { +// SetInstanceExposeAllowlist safely sets the Configuration value for state's 'InstanceExposeAllowlist' field +func (st *ConfigState) SetInstanceExposeAllowlist(v bool) { st.mutex.Lock() defer st.mutex.Unlock() - st.config.InstanceExposeSuspended = v + st.config.InstanceExposeAllowlist = v st.reloadToViper() } -// GetInstanceExposeSuspended safely fetches the value for global configuration 'InstanceExposeSuspended' field -func GetInstanceExposeSuspended() bool { return global.GetInstanceExposeSuspended() } +// GetInstanceExposeAllowlist safely fetches the value for global configuration 'InstanceExposeAllowlist' field +func GetInstanceExposeAllowlist() bool { return global.GetInstanceExposeAllowlist() } -// SetInstanceExposeSuspended safely sets the value for global configuration 'InstanceExposeSuspended' field -func SetInstanceExposeSuspended(v bool) { global.SetInstanceExposeSuspended(v) } +// SetInstanceExposeAllowlist safely sets the value for global configuration 'InstanceExposeAllowlist' field +func SetInstanceExposeAllowlist(v bool) { global.SetInstanceExposeAllowlist(v) } -// InstanceExposeSuspendedWebFlag returns the flag name for the 'InstanceExposeSuspendedWeb' field -func InstanceExposeSuspendedWebFlag() string { return "instance-expose-suspended-web" } +// InstanceExposeAllowlistWebFlag returns the flag name for the 'InstanceExposeAllowlistWeb' field +func InstanceExposeAllowlistWebFlag() string { return "instance-expose-allowlist-web" } -// GetInstanceExposeSuspendedWeb safely fetches the Configuration value for state's 'InstanceExposeSuspendedWeb' field -func (st *ConfigState) GetInstanceExposeSuspendedWeb() (v bool) { +// GetInstanceExposeAllowlistWeb safely fetches the Configuration value for state's 'InstanceExposeAllowlistWeb' field +func (st *ConfigState) GetInstanceExposeAllowlistWeb() (v bool) { st.mutex.RLock() - v = st.config.InstanceExposeSuspendedWeb + v = st.config.InstanceExposeAllowlistWeb st.mutex.RUnlock() return } -// SetInstanceExposeSuspendedWeb safely sets the Configuration value for state's 'InstanceExposeSuspendedWeb' field -func (st *ConfigState) SetInstanceExposeSuspendedWeb(v bool) { +// SetInstanceExposeAllowlistWeb safely sets the Configuration value for state's 'InstanceExposeAllowlistWeb' field +func (st *ConfigState) SetInstanceExposeAllowlistWeb(v bool) { st.mutex.Lock() defer st.mutex.Unlock() - st.config.InstanceExposeSuspendedWeb = v + st.config.InstanceExposeAllowlistWeb = v st.reloadToViper() } -// GetInstanceExposeSuspendedWeb safely fetches the value for global configuration 'InstanceExposeSuspendedWeb' field -func GetInstanceExposeSuspendedWeb() bool { return global.GetInstanceExposeSuspendedWeb() } +// GetInstanceExposeAllowlistWeb safely fetches the value for global configuration 'InstanceExposeAllowlistWeb' field +func GetInstanceExposeAllowlistWeb() bool { return global.GetInstanceExposeAllowlistWeb() } -// SetInstanceExposeSuspendedWeb safely sets the value for global configuration 'InstanceExposeSuspendedWeb' field -func SetInstanceExposeSuspendedWeb(v bool) { global.SetInstanceExposeSuspendedWeb(v) } +// SetInstanceExposeAllowlistWeb safely sets the value for global configuration 'InstanceExposeAllowlistWeb' field +func SetInstanceExposeAllowlistWeb(v bool) { global.SetInstanceExposeAllowlistWeb(v) } // InstanceExposePublicTimelineFlag returns the flag name for the 'InstanceExposePublicTimeline' field func InstanceExposePublicTimelineFlag() string { return "instance-expose-public-timeline" } diff --git a/internal/processing/instance.go b/internal/processing/instance.go index 87fe1e3ef..e1a3785e9 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -19,8 +19,10 @@ package processing import ( "context" + "errors" "fmt" - "sort" + "slices" + "strings" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/config" @@ -31,6 +33,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/text" "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" + "code.superseriousbusiness.org/gotosocial/internal/util/xslices" "code.superseriousbusiness.org/gotosocial/internal/validate" ) @@ -62,70 +65,126 @@ func (p *Processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gt return ai, nil } -func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) { - domains := []*apimodel.Domain{} +func (p *Processor) InstancePeersGet( + ctx context.Context, + includeBlocked bool, + includeAllowed bool, + includeOpen bool, + flatten bool, + includeSeverity bool, +) (any, gtserror.WithCode) { + var ( + domainPerms []gtsmodel.DomainPermission + apiDomains []*apimodel.Domain + ) + + if includeBlocked { + blocks, err := p.state.DB.GetDomainBlocks(ctx) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting domain blocks: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } - if includeOpen { - instances, err := p.state.DB.GetInstancePeers(ctx, false) - if err != nil && err != db.ErrNoEntries { - err = fmt.Errorf("error selecting instance peers: %s", err) + for _, block := range blocks { + domainPerms = append(domainPerms, block) + } + + } else if includeAllowed { + allows, err := p.state.DB.GetDomainAllows(ctx) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting domain allows: %w", err) return nil, gtserror.NewErrorInternalError(err) } - for _, i := range instances { - // Domain may be in Punycode, - // de-punify it just in case. - d, err := util.DePunify(i.Domain) - if err != nil { - log.Errorf(ctx, "couldn't depunify domain %s: %s", i.Domain, err) - continue - } + for _, allow := range allows { + domainPerms = append(domainPerms, allow) + } + } - domains = append(domains, &apimodel.Domain{Domain: d}) + for _, domainPerm := range domainPerms { + // Domain may be in Punycode, + // de-punify it just in case. + domain := domainPerm.GetDomain() + depunied, err := util.DePunify(domain) + if err != nil { + log.Errorf(ctx, "couldn't depunify domain %s: %v", domain, err) + continue } + + if util.PtrOrZero(domainPerm.GetObfuscate()) { + // Obfuscate the de-punified version. + depunied = obfuscate(depunied) + } + + apiDomain := &apimodel.Domain{ + Domain: depunied, + Comment: util.Ptr(domainPerm.GetPublicComment()), + } + + if domainPerm.GetType() == gtsmodel.DomainPermissionBlock { + const severity = "suspend" + apiDomain.Severity = severity + suspendedAt := domainPerm.GetCreatedAt() + apiDomain.SuspendedAt = util.FormatISO8601(suspendedAt) + } + + apiDomains = append(apiDomains, apiDomain) } - if includeSuspended { - domainBlocks := []*gtsmodel.DomainBlock{} - if err := p.state.DB.GetAll(ctx, &domainBlocks); err != nil && err != db.ErrNoEntries { + if includeOpen { + instances, err := p.state.DB.GetInstancePeers(ctx, false) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = gtserror.Newf("db error getting instance peers: %w", err) return nil, gtserror.NewErrorInternalError(err) } - for _, domainBlock := range domainBlocks { + for _, instance := range instances { // Domain may be in Punycode, // de-punify it just in case. - d, err := util.DePunify(domainBlock.Domain) + domain := instance.Domain + depunied, err := util.DePunify(domain) if err != nil { - log.Errorf(ctx, "couldn't depunify domain %s: %s", domainBlock.Domain, err) + log.Errorf(ctx, "couldn't depunify domain %s: %v", domain, err) continue } - if *domainBlock.Obfuscate { - // Obfuscate the de-punified version. - d = obfuscate(d) - } - - domains = append(domains, &apimodel.Domain{ - Domain: d, - SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt), - Comment: &domainBlock.PublicComment, - }) + apiDomains = append( + apiDomains, + &apimodel.Domain{ + Domain: depunied, + }, + ) } } - sort.Slice(domains, func(i, j int) bool { - return domains[i].Domain < domains[j].Domain - }) - - if flat { - flattened := []string{} - for _, d := range domains { - flattened = append(flattened, d.Domain) - } - return flattened, nil + // Sort a-z. + slices.SortFunc( + apiDomains, + func(a, b *apimodel.Domain) int { + return strings.Compare(a.Domain, b.Domain) + }, + ) + + // Deduplicate. + apiDomains = xslices.DeduplicateFunc( + apiDomains, + func(v *apimodel.Domain) string { + return v.Domain + }, + ) + + if flatten { + // Return just the domains. + return xslices.Gather( + []string{}, + apiDomains, + func(v *apimodel.Domain) string { + return v.Domain + }, + ), nil } - return domains, nil + return apiDomains, nil } func (p *Processor) InstanceGetRules(ctx context.Context) ([]apimodel.InstanceRule, gtserror.WithCode) { diff --git a/internal/web/about.go b/internal/web/about.go index e899bcb84..23af503ac 100644 --- a/internal/web/about.go +++ b/internal/web/about.go @@ -57,7 +57,8 @@ func (m *Module) aboutGETHandler(c *gin.Context) { Stylesheets: []string{cssAbout}, Extra: map[string]any{ "showStrap": true, - "blocklistExposed": config.GetInstanceExposeSuspendedWeb(), + "blocklistExposed": config.GetInstanceExposeBlocklistWeb(), + "allowlistExposed": config.GetInstanceExposeAllowlistWeb(), "languages": config.GetInstanceLanguages().DisplayStrs(), }, } diff --git a/internal/web/domain-blocklist.go b/internal/web/domain-blocklist.go deleted file mode 100644 index 34e23d899..000000000 --- a/internal/web/domain-blocklist.go +++ /dev/null @@ -1,75 +0,0 @@ -// 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 web - -import ( - "context" - "fmt" - - apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" - "code.superseriousbusiness.org/gotosocial/internal/config" - "code.superseriousbusiness.org/gotosocial/internal/gtserror" - "github.com/gin-gonic/gin" -) - -const ( - domainBlockListPath = aboutPath + "/suspended" -) - -func (m *Module) domainBlockListGETHandler(c *gin.Context) { - instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) - if errWithCode != nil { - apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) - return - } - - // Return instance we already got from the db, - // don't try to fetch it again when erroring. - instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { - return instance, nil - } - - // We only serve text/html at this endpoint. - if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { - apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet) - return - } - - if !config.GetInstanceExposeSuspendedWeb() { - err := fmt.Errorf("this instance does not publicy expose its blocklist") - apiutil.WebErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), instanceGet) - return - } - - domainBlocks, errWithCode := m.processor.InstancePeersGet(c.Request.Context(), true, false, false) - if errWithCode != nil { - apiutil.WebErrorHandler(c, errWithCode, instanceGet) - return - } - - page := apiutil.WebPage{ - Template: "domain-blocklist.tmpl", - Instance: instance, - OGMeta: apiutil.OGBase(instance), - Stylesheets: []string{cssFA}, - Extra: map[string]any{"blocklist": domainBlocks}, - } - - apiutil.TemplateWebPage(c, page) -} diff --git a/internal/web/domainperms.go b/internal/web/domainperms.go new file mode 100644 index 000000000..24608e1fd --- /dev/null +++ b/internal/web/domainperms.go @@ -0,0 +1,136 @@ +// 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 web + +import ( + "context" + "errors" + + apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" + apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" + "code.superseriousbusiness.org/gotosocial/internal/config" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "github.com/gin-gonic/gin" +) + +const ( + domainBlocklistPath = aboutPath + "/domain_blocks" + domainAllowlistPath = aboutPath + "/domain_allows" +) + +func (m *Module) domainBlocklistGETHandler(c *gin.Context) { + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error()) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + if !config.GetInstanceExposeBlocklistWeb() { + const errText = "this instance does not expose its blocklist via the web" + errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + domainBlocks, errWithCode := m.processor.InstancePeersGet( + c.Request.Context(), + true, // Include blocked. + false, // Don't include allowed. + false, // Don't include open. + false, // Don't flatten list. + false, // Don't include severity. + ) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "domain-blocklist.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssFA}, + Extra: map[string]any{"blocklist": domainBlocks}, + } + + apiutil.TemplateWebPage(c, page) +} + +func (m *Module) domainAllowlistGETHandler(c *gin.Context) { + instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context()) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1) + return + } + + // Return instance we already got from the db, + // don't try to fetch it again when erroring. + instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { + return instance, nil + } + + // We only serve text/html at this endpoint. + if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil { + errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error()) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + if !config.GetInstanceExposeAllowlistWeb() { + const errText = "this instance does not expose its allowlist via the web" + errWithCode := gtserror.NewErrorUnauthorized(errors.New(errText), errText) + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + domainAllows, errWithCode := m.processor.InstancePeersGet( + c.Request.Context(), + false, // Don't include blocked. + true, // Include allowed. + false, // Don't include open. + false, // Don't flatten list. + false, // Don't include severity. + ) + if errWithCode != nil { + apiutil.WebErrorHandler(c, errWithCode, instanceGet) + return + } + + page := apiutil.WebPage{ + Template: "domain-allowlist.tmpl", + Instance: instance, + OGMeta: apiutil.OGBase(instance), + Stylesheets: []string{cssFA}, + Extra: map[string]any{"allowlist": domainAllows}, + } + + apiutil.TemplateWebPage(c, page) +} diff --git a/internal/web/web.go b/internal/web/web.go index 4494f8ff9..c7b7c9f25 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -127,7 +127,8 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) { everythingElseGroup.Handle(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler) everythingElseGroup.Handle(http.MethodGet, aboutPath, m.aboutGETHandler) everythingElseGroup.Handle(http.MethodGet, loginPath, m.loginGETHandler) - everythingElseGroup.Handle(http.MethodGet, domainBlockListPath, m.domainBlockListGETHandler) + everythingElseGroup.Handle(http.MethodGet, domainBlocklistPath, m.domainBlocklistGETHandler) + everythingElseGroup.Handle(http.MethodGet, domainAllowlistPath, m.domainAllowlistGETHandler) everythingElseGroup.Handle(http.MethodGet, tagsPath, m.tagGETHandler) everythingElseGroup.Handle(http.MethodGet, signupPath, m.signupGETHandler) everythingElseGroup.Handle(http.MethodPost, signupPath, m.signupPOSTHandler) |
