Changes
20 changed files (+762/-5)
-
-
@@ -2253,6 +2253,23 @@ PATH =;; ;; Enable/Disable user statistics for nodeinfo if federation is enabled ; SHARE_USER_STATISTICS = true ;; ;; Maximum federation request and response size (MB) ; MAX_SIZE = 4 ;; ;; WARNING: Changing the settings below can break federation. ;; ;; HTTP signature algorithms ; ALGORITHMS = rsa-sha256, rsa-sha512, ed25519 ;; ;; HTTP signature digest algorithm ; DIGEST_ALGORITHM = SHA-256 ;; ;; GET headers for federation requests ; GET_HEADERS = (request-target), Date ;; ;; POST headers for federation requests ; POST_HEADERS = (request-target), Date, Digest ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-
-
@@ -1090,6 +1090,14 @@ Task queue configuration has been moved to `queue.task`. However, the below conf- `ENABLED`: **true**: Enable/Disable federation capabilities - `SHARE_USER_STATISTICS`: **true**: Enable/Disable user statistics for nodeinfo if federation is enabled - `MAX_SIZE`: **4**: Maximum federation request and response size (MB) WARNING: Changing the settings below can break federation. - `ALGORITHMS`: **rsa-sha256, rsa-sha512, ed25519**: HTTP signature algorithms - `DIGEST_ALGORITHM`: **SHA-256**: HTTP signature digest algorithm - `GET_HEADERS`: **(request-target), Date**: GET headers for federation requests - `POST_HEADERS`: **(request-target), Date, Digest**: POST headers for federation requests ## Packages (`packages`)
-
-
-
@@ -28,10 +28,12 @@ require (github.com/ethantkoenig/rupture v1.0.1 github.com/felixge/fgprof v0.9.2 github.com/gliderlabs/ssh v0.3.4 github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d github.com/go-chi/chi/v5 v5.0.7 github.com/go-chi/cors v1.2.1 github.com/go-enry/go-enry/v2 v2.8.2 github.com/go-fed/httpsig v1.1.0 github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e github.com/go-git/go-billy/v5 v5.3.1 github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 github.com/go-ldap/ldap/v3 v3.4.3
-
@@ -107,6 +109,7 @@ require (require ( cloud.google.com/go v0.99.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2 // indirect github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect github.com/Microsoft/go-winio v0.5.2 // indirect github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5 // indirect
-
@@ -160,6 +163,7 @@ require (github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fullstorydev/grpcurl v1.8.1 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect
-
@@ -252,6 +256,7 @@ require (github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/ulikunitz/xz v0.5.10 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/valyala/fastjson v1.6.3 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.1 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
-
-
-
@@ -69,6 +69,8 @@ contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EUcontrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2 h1:2OrsyJYZp7J6nyAsKi2q1SELYRaIc0aQmcQ/EQqPfk8= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb h1:Yy0Bxzc8R2wxiwXoG/rECGplJUSpXqCsog9PuJFgiHs= gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0=
-
@@ -460,6 +462,12 @@ github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b h1:+RjYfEfoZdM3wHFs752dlOpGaoRhwRRyQxjajg08LcQ= github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b/go.mod h1:DE3vvc6Didgfd3k7M1Mos6qMDFNmMrxJmYVMHG9h9Io= github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f h1:kJhGo4NApJP0Lt9lkJnfmuTnRWVFbCynY0kiTxpPUR4= github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f/go.mod h1:KHkKFKZvc05lr79+RGoq/zG8YjWi3+FK60Bxd+mpCew= github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d h1:Z/oRXMlZHjvjIqDma1FrIGL3iE5YL7MUI0bwYEZ6qbA= github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-
@@ -472,8 +480,8 @@ github.com/go-enry/go-enry/v2 v2.8.2 h1:uiGmC+3K8sVd/6DOe2AOJEOihJdqda83nPyJNtMRgithub.com/go-enry/go-enry/v2 v2.8.2/go.mod h1:GVzIiAytiS5uT/QiuakK7TF1u4xDab87Y8V5EJRpsIQ= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
-
@@ -1507,6 +1515,8 @@ github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw=github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc= github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=
-
-
-
@@ -0,0 +1,103 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package integrations import ( "context" "fmt" "net/http" "net/http/httptest" "net/url" "testing" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" "github.com/stretchr/testify/assert" ) func TestActivityPubPerson(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { setting.Federation.Enabled = true defer func() { setting.Federation.Enabled = false }() username := "user2" req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username)) resp := MakeRequest(t, req, http.StatusOK) body := resp.Body.Bytes() assert.Contains(t, string(body), "@context") var person ap.Person err := person.UnmarshalJSON(body) assert.NoError(t, err) assert.Equal(t, ap.PersonType, person.Type) assert.Equal(t, username, person.PreferredUsername.String()) keyID := person.GetID().String() assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyID) assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.Outbox.GetID().String()) assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.Inbox.GetID().String()) pubKey := person.PublicKey assert.NotNil(t, pubKey) publicKeyID := keyID + "#main-key" assert.Equal(t, pubKey.ID.String(), publicKeyID) pubKeyPem := pubKey.PublicKeyPem assert.NotNil(t, pubKeyPem) assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem) }) } func TestActivityPubMissingPerson(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { setting.Federation.Enabled = true defer func() { setting.Federation.Enabled = false }() req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser") resp := MakeRequest(t, req, http.StatusNotFound) assert.Contains(t, resp.Body.String(), "user redirect does not exist") }) } func TestActivityPubPersonInbox(t *testing.T) { srv := httptest.NewServer(c) defer srv.Close() onGiteaRun(t, func(*testing.T, *url.URL) { appURL := setting.AppURL setting.Federation.Enabled = true setting.AppURL = srv.URL defer func() { setting.Federation.Enabled = false setting.Database.LogSQL = false setting.AppURL = appURL }() username1 := "user1" ctx := context.Background() user1, err := user_model.GetUserByName(ctx, username1) assert.NoError(t, err) user1url := fmt.Sprintf("%s/api/v1/activitypub/user/%s#main-key", srv.URL, username1) c, err := activitypub.NewClient(user1, user1url) assert.NoError(t, err) username2 := "user2" user2inboxurl := fmt.Sprintf("%s/api/v1/activitypub/user/%s/inbox", srv.URL, username2) // Signed request succeeds resp, err := c.Post([]byte{}, user2inboxurl) assert.NoError(t, err) assert.Equal(t, http.StatusNoContent, resp.StatusCode) // Unsigned request fails req := NewRequest(t, "POST", user2inboxurl) MakeRequest(t, req, http.StatusInternalServerError) }) }
-
-
-
@@ -52,7 +52,7 @@ func TestWebfinger(t *testing.T) {var jrd webfingerJRD DecodeJSON(t, resp, &jrd) assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject) assert.ElementsMatch(t, []string{user.HTMLURL()}, jrd.Aliases) assert.ElementsMatch(t, []string{user.HTMLURL(), appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(user.Name)}, jrd.Aliases) req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, "unknown.host")) MakeRequest(t, req, http.StatusBadRequest)
-
-
-
@@ -9,4 +9,8 @@ const (SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types" // SettingsKeyDiffWhitespaceBehavior is the setting key for whitespace behavior of diff SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour" // UserActivityPubPrivPem is user's private key UserActivityPubPrivPem = "activitypub.priv_pem" // UserActivityPubPubPem is user's public key UserActivityPubPubPem = "activitypub.pub_pem" )
-
-
-
@@ -0,0 +1,124 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "bytes" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "net/http" "strings" "time" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/setting" "github.com/go-fed/httpsig" ) const ( // ActivityStreamsContentType const ActivityStreamsContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` httpsigExpirationTime = 60 ) // Gets the current time as an RFC 2616 formatted string // RFC 2616 requires RFC 1123 dates but with GMT instead of UTC func CurrentTime() string { return strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT") } func containsRequiredHTTPHeaders(method string, headers []string) error { var hasRequestTarget, hasDate, hasDigest bool for _, header := range headers { hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget hasDate = hasDate || header == "Date" hasDigest = hasDigest || header == "Digest" } if !hasRequestTarget { return fmt.Errorf("missing http header for %s: %s", method, httpsig.RequestTarget) } else if !hasDate { return fmt.Errorf("missing http header for %s: Date", method) } else if !hasDigest && method != http.MethodGet { return fmt.Errorf("missing http header for %s: Digest", method) } return nil } // Client struct type Client struct { client *http.Client algs []httpsig.Algorithm digestAlg httpsig.DigestAlgorithm getHeaders []string postHeaders []string priv *rsa.PrivateKey pubID string } // NewClient function func NewClient(user *user_model.User, pubID string) (c *Client, err error) { if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil { return } else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil { return } priv, err := GetPrivateKey(user) if err != nil { return } privPem, _ := pem.Decode([]byte(priv)) privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) if err != nil { return } c = &Client{ client: &http.Client{ Transport: &http.Transport{ Proxy: proxy.Proxy(), }, }, algs: setting.HttpsigAlgs, digestAlg: httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm), getHeaders: setting.Federation.GetHeaders, postHeaders: setting.Federation.PostHeaders, priv: privParsed, pubID: pubID, } return } // NewRequest function func (c *Client) NewRequest(b []byte, to string) (req *http.Request, err error) { buf := bytes.NewBuffer(b) req, err = http.NewRequest(http.MethodPost, to, buf) if err != nil { return } req.Header.Add("Content-Type", ActivityStreamsContentType) req.Header.Add("Date", CurrentTime()) req.Header.Add("User-Agent", "Gitea/"+setting.AppVer) signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime) if err != nil { return } err = signer.SignRequest(c.priv, c.pubID, req, b) return } // Post function func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) { var req *http.Request if req, err = c.NewRequest(b, to); err != nil { return } resp, err = c.client.Do(req) return }
-
-
-
@@ -0,0 +1,49 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "fmt" "io" "net/http" "net/http/httptest" "regexp" "testing" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models" // https://discourse.gitea.io/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4 "github.com/stretchr/testify/assert" ) func TestActivityPubSignedPost(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) pubID := "https://example.com/pubID" c, err := NewClient(user, pubID) assert.NoError(t, err) expected := "BODY" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest")) assert.Contains(t, r.Header.Get("Signature"), pubID) assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType) body, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, expected, string(body)) fmt.Fprintf(w, expected) })) defer srv.Close() r, err := c.Post([]byte(expected), srv.URL) assert.NoError(t, err) defer r.Body.Close() body, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, expected, string(body)) }
-
-
-
@@ -0,0 +1,18 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "path/filepath" "testing" "code.gitea.io/gitea/models/unittest" ) func TestMain(m *testing.M) { unittest.MainTest(m, &unittest.TestOptions{ GiteaRootPath: filepath.Join("..", ".."), }) }
-
-
-
@@ -0,0 +1,45 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( user_model "code.gitea.io/gitea/models/user" ) // GetKeyPair function returns a user's private and public keys func GetKeyPair(user *user_model.User) (pub, priv string, err error) { var settings map[string]*user_model.Setting settings, err = user_model.GetUserSettings(user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem}) if err != nil { return } else if len(settings) == 0 { if priv, pub, err = GenerateKeyPair(); err != nil { return } if err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPrivPem, priv); err != nil { return } if err = user_model.SetUserSetting(user.ID, user_model.UserActivityPubPubPem, pub); err != nil { return } return } else { priv = settings[user_model.UserActivityPubPrivPem].SettingValue pub = settings[user_model.UserActivityPubPubPem].SettingValue return } } // GetPublicKey function returns a user's public key func GetPublicKey(user *user_model.User) (pub string, err error) { pub, _, err = GetKeyPair(user) return } // GetPrivateKey function returns a user's private key func GetPrivateKey(user *user_model.User) (priv string, err error) { _, priv, err = GetKeyPair(user) return }
-
-
-
@@ -0,0 +1,29 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "testing" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" _ "code.gitea.io/gitea/models" // https://discourse.gitea.io/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4 "github.com/stretchr/testify/assert" ) func TestUserSettings(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) pub, priv, err := GetKeyPair(user1) assert.NoError(t, err) pub1, err := GetPublicKey(user1) assert.NoError(t, err) assert.Equal(t, pub, pub1) priv1, err := GetPrivateKey(user1) assert.NoError(t, err) assert.Equal(t, priv, priv1) }
-
-
-
@@ -4,21 +4,49 @@package setting import "code.gitea.io/gitea/modules/log" import ( "code.gitea.io/gitea/modules/log" "github.com/go-fed/httpsig" ) // Federation settings var ( Federation = struct { Enabled bool ShareUserStatistics bool MaxSize int64 Algorithms []string DigestAlgorithm string GetHeaders []string PostHeaders []string }{ Enabled: true, ShareUserStatistics: true, MaxSize: 4, Algorithms: []string{"rsa-sha256", "rsa-sha512", "ed25519"}, DigestAlgorithm: "SHA-256", GetHeaders: []string{"(request-target)", "Date"}, PostHeaders: []string{"(request-target)", "Date", "Digest"}, } ) // Constant slice of httpsig algorithm objects var HttpsigAlgs []httpsig.Algorithm func newFederationService() { if err := Cfg.Section("federation").MapTo(&Federation); err != nil { log.Fatal("Failed to map Federation settings: %v", err) } else if !httpsig.IsSupportedDigestAlgorithm(Federation.DigestAlgorithm) { log.Fatal("unsupported digest algorithm: %s", Federation.DigestAlgorithm) return } // Get MaxSize in bytes instead of MiB Federation.MaxSize = 1 << 20 * Federation.MaxSize HttpsigAlgs = make([]httpsig.Algorithm, len(Federation.Algorithms)) for i, alg := range Federation.Algorithms { HttpsigAlgs[i] = httpsig.Algorithm(alg) } }
-
-
-
@@ -0,0 +1,10 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package structs // ActivityPub type type ActivityPub struct { Context string `json:"@context"` }
-
-
-
@@ -0,0 +1,106 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "net/http" "strings" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" "github.com/go-ap/jsonld" ) // Person function returns the Person actor for a user func Person(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username} activitypub activitypubPerson // --- // summary: Returns the Person actor for a user // produces: // - application/json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // "200": // "$ref": "#/responses/ActivityPub" link := strings.TrimSuffix(setting.AppURL, "/") + "/api/v1/activitypub/user/" + ctx.ContextUser.Name person := ap.PersonNew(ap.IRI(link)) person.Name = ap.NaturalLanguageValuesNew() err := person.Name.Set("en", ap.Content(ctx.ContextUser.FullName)) if err != nil { ctx.ServerError("Set Name", err) return } person.PreferredUsername = ap.NaturalLanguageValuesNew() err = person.PreferredUsername.Set("en", ap.Content(ctx.ContextUser.Name)) if err != nil { ctx.ServerError("Set PreferredUsername", err) return } person.URL = ap.IRI(ctx.ContextUser.HTMLURL()) person.Icon = ap.Image{ Type: ap.ImageType, MediaType: "image/png", URL: ap.IRI(ctx.ContextUser.AvatarLink()), } person.Inbox = ap.IRI(link + "/inbox") person.Outbox = ap.IRI(link + "/outbox") person.PublicKey.ID = ap.IRI(link + "#main-key") person.PublicKey.Owner = ap.IRI(link) publicKeyPem, err := activitypub.GetPublicKey(ctx.ContextUser) if err != nil { ctx.ServerError("GetPublicKey", err) return } person.PublicKey.PublicKeyPem = publicKeyPem binary, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI), jsonld.IRI(ap.SecurityContextURI)).Marshal(person) if err != nil { ctx.ServerError("MarshalJSON", err) return } ctx.Resp.Header().Add("Content-Type", activitypub.ActivityStreamsContentType) ctx.Resp.WriteHeader(http.StatusOK) if _, err = ctx.Resp.Write(binary); err != nil { log.Error("write to resp err: %v", err) } } // PersonInbox function handles the incoming data for a user inbox func PersonInbox(ctx *context.APIContext) { // swagger:operation POST /activitypub/user/{username}/inbox activitypub activitypubPersonInbox // --- // summary: Send to the inbox // produces: // - application/json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // responses: // "204": // "$ref": "#/responses/empty" ctx.Status(http.StatusNoContent) }
-
-
-
@@ -0,0 +1,102 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "crypto" "crypto/x509" "encoding/pem" "fmt" "io" "net/http" "net/url" "code.gitea.io/gitea/modules/activitypub" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/setting" ap "github.com/go-ap/activitypub" "github.com/go-fed/httpsig" ) func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) { person := ap.PersonNew(ap.IRI(keyID.String())) err = person.UnmarshalJSON(b) if err != nil { err = fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %v", err) return } pubKey := person.PublicKey if pubKey.ID.String() != keyID.String() { err = fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, string(b)) return } pubKeyPem := pubKey.PublicKeyPem block, _ := pem.Decode([]byte(pubKeyPem)) if block == nil || block.Type != "PUBLIC KEY" { err = fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") return } p, err = x509.ParsePKIXPublicKey(block.Bytes) return } func fetch(iri *url.URL) (b []byte, err error) { req := httplib.NewRequest(iri.String(), http.MethodGet) req.Header("Accept", activitypub.ActivityStreamsContentType) req.Header("User-Agent", "Gitea/"+setting.AppVer) resp, err := req.Response() if err != nil { return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status) return } b, err = io.ReadAll(io.LimitReader(resp.Body, setting.Federation.MaxSize)) return } func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) { r := ctx.Req // 1. Figure out what key we need to verify v, err := httpsig.NewVerifier(r) if err != nil { return } ID := v.KeyId() idIRI, err := url.Parse(ID) if err != nil { return } // 2. Fetch the public key of the other actor b, err := fetch(idIRI) if err != nil { return } pubKey, err := getPublicKeyFromResponse(b, idIRI) if err != nil { return } // 3. Verify the other actor's key algo := httpsig.Algorithm(setting.Federation.Algorithms[0]) authenticated = v.Verify(pubKey, algo) == nil return } // ReqHTTPSignature function func ReqHTTPSignature() func(ctx *gitea_context.APIContext) { return func(ctx *gitea_context.APIContext) { if authenticated, err := verifyHTTPSignatures(ctx); err != nil { ctx.ServerError("verifyHttpSignatures", err) } else if !authenticated { ctx.Error(http.StatusForbidden, "reqSignature", "request signature verification failed") } } }
-
-
-
@@ -81,6 +81,7 @@ import ("code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/activitypub" "code.gitea.io/gitea/routers/api/v1/admin" "code.gitea.io/gitea/routers/api/v1/misc" "code.gitea.io/gitea/routers/api/v1/notify"
-
@@ -643,6 +644,12 @@ func Routes() *web.Route {m.Get("/version", misc.Version) if setting.Federation.Enabled { m.Get("/nodeinfo", misc.NodeInfo) m.Group("/activitypub", func() { m.Group("/user/{username}", func() { m.Get("", activitypub.Person) m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox) }, context_service.UserAssignmentAPI()) }) } m.Get("/signing-key.gpg", misc.SigningKey) m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
-
-
-
@@ -0,0 +1,16 @@// Copyright 2022 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package swagger import ( api "code.gitea.io/gitea/modules/structs" ) // ActivityPub // swagger:response ActivityPub type swaggerResponseActivityPub struct { // in:body Body api.ActivityPub `json:"body"` }
-
-
-
@@ -86,6 +86,7 @@ func WebfingerQuery(ctx *context.Context) {aliases := []string{ u.HTMLURL(), appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(u.Name), } if !u.KeepEmailPrivate { aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email))
-
@@ -101,8 +102,14 @@ func WebfingerQuery(ctx *context.Context) {Rel: "http://webfinger.net/rel/avatar", Href: u.AvatarLink(), }, { Rel: "self", Type: "application/activity+json", Href: appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(u.Name), }, } ctx.Resp.Header().Add("Access-Control-Allow-Origin", "*") ctx.JSON(http.StatusOK, &webfingerJRD{ Subject: fmt.Sprintf("acct:%s@%s", url.QueryEscape(u.Name), appURL.Host), Aliases: aliases,
-
-
-
@@ -23,6 +23,58 @@}, "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1", "paths": { "/activitypub/user/{username}": { "get": { "produces": [ "application/json" ], "tags": [ "activitypub" ], "summary": "Returns the Person actor for a user", "operationId": "activitypubPerson", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true } ], "responses": { "200": { "$ref": "#/responses/ActivityPub" } } } }, "/activitypub/user/{username}/inbox": { "post": { "produces": [ "application/json" ], "tags": [ "activitypub" ], "summary": "Send to the inbox", "operationId": "activitypubPersonInbox", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true } ], "responses": { "204": { "$ref": "#/responses/empty" } } } }, "/admin/cron": { "get": { "produces": [
-
@@ -13113,6 +13165,17 @@}, "x-go-package": "code.gitea.io/gitea/modules/structs" }, "ActivityPub": { "description": "ActivityPub type", "type": "object", "properties": { "@context": { "type": "string", "x-go-name": "Context" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, "AddCollaboratorOption": { "description": "AddCollaboratorOption options when adding a user as a collaborator of a repository", "type": "object",
-
@@ -18794,6 +18857,12 @@} } }, "ActivityPub": { "description": "ActivityPub", "schema": { "$ref": "#/definitions/ActivityPub" } }, "AnnotatedTag": { "description": "AnnotatedTag", "schema": {
-