Merge pull request 'Federation: Parse ActorId & cache FederationHost' (#3662) from meissa/forgejo:forgejo-federated-parse-actorId into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3662 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
77092c7a0c
17 changed files with 1202 additions and 14 deletions
52
models/forgefed/federationhost.go
Normal file
52
models/forgefed/federationhost.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
)
|
||||
|
||||
// FederationHost data type
|
||||
// swagger:model
|
||||
type FederationHost struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
||||
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
||||
LatestActivity time.Time `xorm:"NOT NULL"`
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// Factory function for FederationHost. Created struct is asserted to be valid.
|
||||
func NewFederationHost(nodeInfo NodeInfo, hostFqdn string) (FederationHost, error) {
|
||||
result := FederationHost{
|
||||
HostFqdn: strings.ToLower(hostFqdn),
|
||||
NodeInfo: nodeInfo,
|
||||
}
|
||||
if valid, err := validation.IsValid(result); !valid {
|
||||
return FederationHost{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Validate collects error strings in a slice and returns this
|
||||
func (host FederationHost) Validate() []string {
|
||||
var result []string
|
||||
result = append(result, validation.ValidateNotEmpty(host.HostFqdn, "HostFqdn")...)
|
||||
result = append(result, validation.ValidateMaxLen(host.HostFqdn, 255, "HostFqdn")...)
|
||||
result = append(result, host.NodeInfo.Validate()...)
|
||||
if host.HostFqdn != strings.ToLower(host.HostFqdn) {
|
||||
result = append(result, fmt.Sprintf("HostFqdn has to be lower case but was: %v", host.HostFqdn))
|
||||
}
|
||||
if !host.LatestActivity.IsZero() && host.LatestActivity.After(time.Now().Add(10*time.Minute)) {
|
||||
result = append(result, fmt.Sprintf("Latest Activity cannot be in the far future: %v", host.LatestActivity))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
61
models/forgefed/federationhost_repository.go
Normal file
61
models/forgefed/federationhost_repository.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(FederationHost))
|
||||
}
|
||||
|
||||
func GetFederationHost(ctx context.Context, ID int64) (*FederationHost, error) {
|
||||
host := new(FederationHost)
|
||||
has, err := db.GetEngine(ctx).Where("id=?", ID).Get(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, fmt.Errorf("FederationInfo record %v does not exist", ID)
|
||||
}
|
||||
if res, err := validation.IsValid(host); !res {
|
||||
return nil, err
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
|
||||
func FindFederationHostByFqdn(ctx context.Context, fqdn string) (*FederationHost, error) {
|
||||
host := new(FederationHost)
|
||||
has, err := db.GetEngine(ctx).Where("host_fqdn=?", strings.ToLower(fqdn)).Get(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, nil
|
||||
}
|
||||
if res, err := validation.IsValid(host); !res {
|
||||
return nil, err
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
|
||||
func CreateFederationHost(ctx context.Context, host *FederationHost) error {
|
||||
if res, err := validation.IsValid(host); !res {
|
||||
return err
|
||||
}
|
||||
_, err := db.GetEngine(ctx).Insert(host)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateFederationHost(ctx context.Context, host *FederationHost) error {
|
||||
if res, err := validation.IsValid(host); !res {
|
||||
return err
|
||||
}
|
||||
_, err := db.GetEngine(ctx).ID(host.ID).Update(host)
|
||||
return err
|
||||
}
|
78
models/forgefed/federationhost_test.go
Normal file
78
models/forgefed/federationhost_test.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
)
|
||||
|
||||
func Test_FederationHostValidation(t *testing.T) {
|
||||
sut := FederationHost{
|
||||
HostFqdn: "host.do.main",
|
||||
NodeInfo: NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
LatestActivity: time.Now(),
|
||||
}
|
||||
if res, err := validation.IsValid(sut); !res {
|
||||
t.Errorf("sut should be valid but was %q", err)
|
||||
}
|
||||
|
||||
sut = FederationHost{
|
||||
HostFqdn: "",
|
||||
NodeInfo: NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
LatestActivity: time.Now(),
|
||||
}
|
||||
if res, _ := validation.IsValid(sut); res {
|
||||
t.Errorf("sut should be invalid: HostFqdn empty")
|
||||
}
|
||||
|
||||
sut = FederationHost{
|
||||
HostFqdn: strings.Repeat("fill", 64),
|
||||
NodeInfo: NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
LatestActivity: time.Now(),
|
||||
}
|
||||
if res, _ := validation.IsValid(sut); res {
|
||||
t.Errorf("sut should be invalid: HostFqdn too long (len=256)")
|
||||
}
|
||||
|
||||
sut = FederationHost{
|
||||
HostFqdn: "host.do.main",
|
||||
NodeInfo: NodeInfo{},
|
||||
LatestActivity: time.Now(),
|
||||
}
|
||||
if res, _ := validation.IsValid(sut); res {
|
||||
t.Errorf("sut should be invalid: NodeInfo invalid")
|
||||
}
|
||||
|
||||
sut = FederationHost{
|
||||
HostFqdn: "host.do.main",
|
||||
NodeInfo: NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
LatestActivity: time.Now().Add(1 * time.Hour),
|
||||
}
|
||||
if res, _ := validation.IsValid(sut); res {
|
||||
t.Errorf("sut should be invalid: Future timestamp")
|
||||
}
|
||||
|
||||
sut = FederationHost{
|
||||
HostFqdn: "hOst.do.main",
|
||||
NodeInfo: NodeInfo{
|
||||
SoftwareName: "forgejo",
|
||||
},
|
||||
LatestActivity: time.Now(),
|
||||
}
|
||||
if res, _ := validation.IsValid(sut); res {
|
||||
t.Errorf("sut should be invalid: HostFqdn lower case")
|
||||
}
|
||||
}
|
123
models/forgefed/nodeinfo.go
Normal file
123
models/forgefed/nodeinfo.go
Normal file
|
@ -0,0 +1,123 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// ToDo: Search for full text SourceType and Source, also in .md files
|
||||
type (
|
||||
SoftwareNameType string
|
||||
)
|
||||
|
||||
const (
|
||||
ForgejoSourceType SoftwareNameType = "forgejo"
|
||||
GiteaSourceType SoftwareNameType = "gitea"
|
||||
)
|
||||
|
||||
var KnownSourceTypes = []any{
|
||||
ForgejoSourceType, GiteaSourceType,
|
||||
}
|
||||
|
||||
// ------------------------------------------------ NodeInfoWellKnown ------------------------------------------------
|
||||
|
||||
// NodeInfo data type
|
||||
// swagger:model
|
||||
type NodeInfoWellKnown struct {
|
||||
Href string
|
||||
}
|
||||
|
||||
// Factory function for NodeInfoWellKnown. Created struct is asserted to be valid.
|
||||
func NewNodeInfoWellKnown(body []byte) (NodeInfoWellKnown, error) {
|
||||
result, err := NodeInfoWellKnownUnmarshalJSON(body)
|
||||
if err != nil {
|
||||
return NodeInfoWellKnown{}, err
|
||||
}
|
||||
|
||||
if valid, err := validation.IsValid(result); !valid {
|
||||
return NodeInfoWellKnown{}, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NodeInfoWellKnownUnmarshalJSON(data []byte) (NodeInfoWellKnown, error) {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return NodeInfoWellKnown{}, err
|
||||
}
|
||||
href := string(val.GetStringBytes("links", "0", "href"))
|
||||
return NodeInfoWellKnown{Href: href}, nil
|
||||
}
|
||||
|
||||
// Validate collects error strings in a slice and returns this
|
||||
func (node NodeInfoWellKnown) Validate() []string {
|
||||
var result []string
|
||||
result = append(result, validation.ValidateNotEmpty(node.Href, "Href")...)
|
||||
|
||||
parsedURL, err := url.Parse(node.Href)
|
||||
if err != nil {
|
||||
result = append(result, err.Error())
|
||||
return result
|
||||
}
|
||||
|
||||
if parsedURL.Host == "" {
|
||||
result = append(result, "Href has to be absolute")
|
||||
}
|
||||
|
||||
result = append(result, validation.ValidateOneOf(parsedURL.Scheme, []any{"http", "https"}, "parsedURL.Scheme")...)
|
||||
|
||||
if parsedURL.RawQuery != "" {
|
||||
result = append(result, "Href may not contain query")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ------------------------------------------------ NodeInfo ------------------------------------------------
|
||||
|
||||
// NodeInfo data type
|
||||
// swagger:model
|
||||
type NodeInfo struct {
|
||||
SoftwareName SoftwareNameType
|
||||
}
|
||||
|
||||
func NodeInfoUnmarshalJSON(data []byte) (NodeInfo, error) {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return NodeInfo{}, err
|
||||
}
|
||||
source := string(val.GetStringBytes("software", "name"))
|
||||
result := NodeInfo{}
|
||||
result.SoftwareName = SoftwareNameType(source)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewNodeInfo(body []byte) (NodeInfo, error) {
|
||||
result, err := NodeInfoUnmarshalJSON(body)
|
||||
if err != nil {
|
||||
return NodeInfo{}, err
|
||||
}
|
||||
|
||||
if valid, err := validation.IsValid(result); !valid {
|
||||
return NodeInfo{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Validate collects error strings in a slice and returns this
|
||||
func (node NodeInfo) Validate() []string {
|
||||
var result []string
|
||||
result = append(result, validation.ValidateNotEmpty(string(node.SoftwareName), "node.SoftwareName")...)
|
||||
result = append(result, validation.ValidateOneOf(node.SoftwareName, KnownSourceTypes, "node.SoftwareName")...)
|
||||
|
||||
return result
|
||||
}
|
92
models/forgefed/nodeinfo_test.go
Normal file
92
models/forgefed/nodeinfo_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
)
|
||||
|
||||
func Test_NodeInfoWellKnownUnmarshalJSON(t *testing.T) {
|
||||
type testPair struct {
|
||||
item []byte
|
||||
want NodeInfoWellKnown
|
||||
wantErr error
|
||||
}
|
||||
|
||||
tests := map[string]testPair{
|
||||
"with href": {
|
||||
item: []byte(`{"links":[{"href":"https://federated-repo.prod.meissa.de/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`),
|
||||
want: NodeInfoWellKnown{
|
||||
Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo",
|
||||
},
|
||||
},
|
||||
"empty": {
|
||||
item: []byte(``),
|
||||
wantErr: fmt.Errorf("cannot parse JSON: cannot parse empty string; unparsed tail: \"\""),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := NodeInfoWellKnownUnmarshalJSON(tt.item)
|
||||
if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
|
||||
t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("UnmarshalJSON() got = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NodeInfoWellKnownValidate(t *testing.T) {
|
||||
sut := NodeInfoWellKnown{Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo"}
|
||||
if b, err := validation.IsValid(sut); !b {
|
||||
t.Errorf("sut should be valid, %v, %v", sut, err)
|
||||
}
|
||||
|
||||
sut = NodeInfoWellKnown{Href: "./federated-repo.prod.meissa.de/api/v1/nodeinfo"}
|
||||
_, err := validation.IsValid(sut)
|
||||
if !validation.IsErrNotValid(err) && strings.Contains(err.Error(), "Href has to be absolute\nValue is not contained in allowed values [http https]") {
|
||||
t.Errorf("validation error expected but was: %v\n", err)
|
||||
}
|
||||
|
||||
sut = NodeInfoWellKnown{Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo?alert=1"}
|
||||
_, err = validation.IsValid(sut)
|
||||
if !validation.IsErrNotValid(err) && strings.Contains(err.Error(), "Href has to be absolute\nValue is not contained in allowed values [http https]") {
|
||||
t.Errorf("sut should be valid, %v, %v", sut, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewNodeInfoWellKnown(t *testing.T) {
|
||||
sut, _ := NewNodeInfoWellKnown([]byte(`{"links":[{"href":"https://federated-repo.prod.meissa.de/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`))
|
||||
expected := NodeInfoWellKnown{Href: "https://federated-repo.prod.meissa.de/api/v1/nodeinfo"}
|
||||
if sut != expected {
|
||||
t.Errorf("expected was: %v but was: %v", expected, sut)
|
||||
}
|
||||
|
||||
_, err := NewNodeInfoWellKnown([]byte(`invalid`))
|
||||
if err == nil {
|
||||
t.Errorf("error was expected here")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NewNodeInfo(t *testing.T) {
|
||||
sut, _ := NewNodeInfo([]byte(`{"version":"2.1","software":{"name":"gitea","version":"1.20.0+dev-2539-g5840cc6d3","repository":"https://github.com/go-gitea/gitea.git","homepage":"https://gitea.io/"},"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},"openRegistrations":true,"usage":{"users":{"total":13,"activeHalfyear":1,"activeMonth":1}},"metadata":{}}`))
|
||||
expected := NodeInfo{SoftwareName: "gitea"}
|
||||
if sut != expected {
|
||||
t.Errorf("expected was: %v but was: %v", expected, sut)
|
||||
}
|
||||
|
||||
_, err := NewNodeInfo([]byte(`invalid`))
|
||||
if err == nil {
|
||||
t.Errorf("error was expected here")
|
||||
}
|
||||
}
|
|
@ -66,6 +66,8 @@ var migrations = []*Migration{
|
|||
NewMigration("Add `hide_archive_links` column to `release` table", AddHideArchiveLinksToRelease),
|
||||
// v14 -> v15
|
||||
NewMigration("Remove Gitea-specific columns from the repository and badge tables", RemoveGiteaSpecificColumnsFromRepositoryAndBadge),
|
||||
// v15 -> v16
|
||||
NewMigration("Create the `federation_host` table", CreateFederationHostTable),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
33
models/forgejo_migrations/v15.go
Normal file
33
models/forgejo_migrations/v15.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgejo_migrations //nolint:revive
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type (
|
||||
SoftwareNameType string
|
||||
)
|
||||
|
||||
type NodeInfo struct {
|
||||
SoftwareName SoftwareNameType
|
||||
}
|
||||
|
||||
type FederationHost struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
|
||||
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
|
||||
LatestActivity time.Time `xorm:"NOT NULL"`
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
func CreateFederationHostTable(x *xorm.Engine) error {
|
||||
return x.Sync(new(FederationHost))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue