Skip to content

Commit 15616bd

Browse files
Anthony WangSysoev, Vladimir
Anthony Wang
authored and
Sysoev, Vladimir
committed
User keypairs and HTTP signatures for ActivityPub federation using go-ap (go-gitea#19133)
* go.mod: add go-fed/{httpsig,activity/pub,activity/streams} dependency go get github.com/go-fed/activity/streams@master go get github.com/go-fed/activity/pub@master go get github.com/go-fed/httpsig@master * activitypub: implement /api/v1/activitypub/user/{username} (go-gitea#14186) Return informations regarding a Person (as defined in ActivityStreams https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person). Refs: go-gitea#14186 Signed-off-by: Loïc Dachary <loic@dachary.org> * activitypub: add the public key to Person (go-gitea#14186) Refs: go-gitea#14186 Signed-off-by: Loïc Dachary <loic@dachary.org> * activitypub: go-fed conformant Clock instance Signed-off-by: Loïc Dachary <loic@dachary.org> * activitypub: signing http client Signed-off-by: Loïc Dachary <loic@dachary.org> * activitypub: implement the ReqSignature middleware Signed-off-by: Loïc Dachary <loic@dachary.org> * activitypub: hack_16834 Signed-off-by: Loïc Dachary <loic@dachary.org> * Fix CI checks-backend errors with go mod tidy Signed-off-by: Anthony Wang <ta180m@pm.me> * Change 2021 to 2022, properly format package imports Signed-off-by: Anthony Wang <ta180m@pm.me> * Run make fmt and make generate-swagger Signed-off-by: Anthony Wang <ta180m@pm.me> * Use Gitea JSON library, add assert for pkp Signed-off-by: Anthony Wang <ta180m@pm.me> * Run make fmt again, fix err var redeclaration Signed-off-by: Anthony Wang <ta180m@pm.me> * Remove LogSQL from ActivityPub person test Signed-off-by: Anthony Wang <ta180m@pm.me> * Assert if json.Unmarshal succeeds Signed-off-by: Anthony Wang <ta180m@pm.me> * Cleanup, handle invalid usernames for ActivityPub person GET request Signed-off-by: Anthony Wang <ta180m@pm.me> * Rename hack_16834 to user_settings Signed-off-by: Anthony Wang <ta180m@pm.me> * Use the httplib module instead of http for GET requests * Clean up whitespace with make fmt * Use time.RFC1123 and make the http.Client proxy-aware * Check if digest algo is supported in setting module * Clean up some variable declarations * Remove unneeded copy * Use system timezone instead of setting.DefaultUILocation * Use named constant for httpsigExpirationTime * Make pubKey IRI #main-key instead of /#main-key * Move /#main-key to #main-key in tests * Implemented Webfinger endpoint. * Add visible check. * Add user profile as alias. * Add actor IRI and remote interaction URL to WebFinger response * fmt * Fix lint errors * Use go-ap instead of go-fed * Run go mod tidy to fix missing modules in go.mod and go.sum * make fmt * Convert remaining code to go-ap * Clean up go.sum * Fix JSON unmarshall error * Fix CI errors by adding @context to Person() and making sure types match * Correctly decode JSON in api_activitypub_person_test.go * Force CI rerun * Fix TestActivityPubPersonInbox segfault * Fix lint error * Use @mariusor's suggestions for idiomatic go-ap usage * Correctly add inbox/outbox IRIs to person * Code cleanup * Remove another LogSQL from ActivityPub person test * Move httpsig algos slice to an init() function * Add actor IRI and remote interaction URL to WebFinger response * Update TestWebFinger to check for ActivityPub IRI in aliases * make fmt * Force CI rerun * WebFinger: Add CORS header and fix Href -> Template for remote interactions The CORS header is needed due to https://datatracker.ietf.org/doc/html/rfc7033#section-5 and fixes some Peertube <-> Gitea federation issues * make lint-backend * Make sure Person endpoint has Content-Type application/activity+json and includes PreferredUsername, URL, and Icon Setting the correct Content-Type is essential for federating with Mastodon * Use UTC instead of GMT * Rename pkey to pubKey * Make sure HTTP request Date in GMT * make fmt * dont drop err * Make sure API responses always refer to username in original case Copied from what I wrote on go-gitea#19133 discussion: Handling username case is a very tricky issue and I've already encountered a Mastodon <-> Gitea federation bug due to Gitea considering Ta180m and ta180m to be the same user while Mastodon thinks they are two different users. I think the best way forward is for Gitea to only use the original case version of the username for federation so other AP software don't get confused. * Move httpsig algs constant slice to modules/setting/federation.go * Add new federation settings to app.example.ini and config-cheat-sheet * Return if marshalling error * Make sure Person IRIs are generated correctly This commit ensures that if the setting.AppURL is something like "http://127.0.0.1:42567" (like in the integration tests), a trailing slash will be added after that URL. * If httpsig verification fails, fix Host header and try again This fixes a very rare bug when Gitea and another AP server (confirmed to happen with Mastodon) are running on the same machine, Gitea fails to verify incoming HTTP signatures. This is because the other AP server creates the sig with the public Gitea domain as the Host. However, when Gitea receives the request, the Host header is instead localhost, so the signature verification fails. Manually changing the host header to the correct value and trying the veification again fixes the bug. * Revert "If httpsig verification fails, fix Host header and try again" This reverts commit f53e46c. The bug was actually caused by nginx messing up the Host header when reverse-proxying since I didn't have the line `proxy_set_header Host $host;` in my nginx config for Gitea. * Go back to using ap.IRI to generate inbox and outbox IRIs * use const for key values * Update routers/web/webfinger.go * Use ctx.JSON in Person response to make code cleaner * Revert "Use ctx.JSON in Person response to make code cleaner" This doesn't work because the ctx.JSON() function already sends the response out and it's too late to edit the headers. This reverts commit 95aad98. * Use activitypub.ActivityStreamsContentType for Person response Content Type * Limit maximum ActivityPub request and response sizes to a configurable setting * Move setting key constants to models/user/setting_keys.go * Fix failing ActivityPubPerson integration test by checking the correct field for username * Add a warning about changing settings that can break federation * Add better comments * Don't multiply Federation.MaxSize by 1<<20 twice * Add more better comments * Fix failing ActivityPubMissingPerson test We now use ctx.ContextUser so the message printed out when a user does not exist is slightly different * make generate-swagger For some reason I didn't realize that /templates/swagger/v1_json.tmpl was machine-generated by make generate-swagger... I've been editing it by hand for three months! 🤦 * Move getting the RFC 2616 time to a separate function * More code cleanup * Update go-ap to fix empty liked collection and removed unneeded HTTP headers * go mod tidy * Add ed25519 to httpsig algorithms * Use go-ap/jsonld to add @context and marshal JSON * Change Gitea user agent from the default to Gitea/Version * Use ctx.ServerError and remove all remote interaction code from webfinger.go
1 parent 2cea89c commit 15616bd

File tree

20 files changed

+762
-5
lines changed

20 files changed

+762
-5
lines changed

Diff for: custom/conf/app.example.ini

+17
Original file line numberDiff line numberDiff line change
@@ -2253,6 +2253,23 @@ PATH =
22532253
;;
22542254
;; Enable/Disable user statistics for nodeinfo if federation is enabled
22552255
; SHARE_USER_STATISTICS = true
2256+
;;
2257+
;; Maximum federation request and response size (MB)
2258+
; MAX_SIZE = 4
2259+
;;
2260+
;; WARNING: Changing the settings below can break federation.
2261+
;;
2262+
;; HTTP signature algorithms
2263+
; ALGORITHMS = rsa-sha256, rsa-sha512, ed25519
2264+
;;
2265+
;; HTTP signature digest algorithm
2266+
; DIGEST_ALGORITHM = SHA-256
2267+
;;
2268+
;; GET headers for federation requests
2269+
; GET_HEADERS = (request-target), Date
2270+
;;
2271+
;; POST headers for federation requests
2272+
; POST_HEADERS = (request-target), Date, Digest
22562273

22572274
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
22582275
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Diff for: docs/content/doc/advanced/config-cheat-sheet.en-us.md

+8
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,14 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
10901090

10911091
- `ENABLED`: **true**: Enable/Disable federation capabilities
10921092
- `SHARE_USER_STATISTICS`: **true**: Enable/Disable user statistics for nodeinfo if federation is enabled
1093+
- `MAX_SIZE`: **4**: Maximum federation request and response size (MB)
1094+
1095+
WARNING: Changing the settings below can break federation.
1096+
1097+
- `ALGORITHMS`: **rsa-sha256, rsa-sha512, ed25519**: HTTP signature algorithms
1098+
- `DIGEST_ALGORITHM`: **SHA-256**: HTTP signature digest algorithm
1099+
- `GET_HEADERS`: **(request-target), Date**: GET headers for federation requests
1100+
- `POST_HEADERS`: **(request-target), Date, Digest**: POST headers for federation requests
10931101

10941102
## Packages (`packages`)
10951103

Diff for: go.mod

+6-1
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ require (
2828
github.com/ethantkoenig/rupture v1.0.1
2929
github.com/felixge/fgprof v0.9.2
3030
github.com/gliderlabs/ssh v0.3.4
31+
github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b
32+
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d
3133
github.com/go-chi/chi/v5 v5.0.7
3234
github.com/go-chi/cors v1.2.1
3335
github.com/go-enry/go-enry/v2 v2.8.2
34-
github.com/go-fed/httpsig v1.1.0
36+
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e
3537
github.com/go-git/go-billy/v5 v5.3.1
3638
github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4
3739
github.com/go-ldap/ldap/v3 v3.4.3
@@ -107,6 +109,7 @@ require (
107109

108110
require (
109111
cloud.google.com/go v0.99.0 // indirect
112+
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2 // indirect
110113
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect
111114
github.com/Microsoft/go-winio v0.5.2 // indirect
112115
github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5 // indirect
@@ -160,6 +163,7 @@ require (
160163
github.com/fsnotify/fsnotify v1.5.4 // indirect
161164
github.com/fullstorydev/grpcurl v1.8.1 // indirect
162165
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
166+
github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f // indirect
163167
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
164168
github.com/go-enry/go-oniguruma v1.2.1 // indirect
165169
github.com/go-git/gcfg v1.5.0 // indirect
@@ -252,6 +256,7 @@ require (
252256
github.com/toqueteos/webbrowser v1.2.0 // indirect
253257
github.com/ulikunitz/xz v0.5.10 // indirect
254258
github.com/unknwon/com v1.0.1 // indirect
259+
github.com/valyala/fastjson v1.6.3 // indirect
255260
github.com/x448/float16 v0.8.4 // indirect
256261
github.com/xanzy/ssh-agent v0.3.1 // indirect
257262
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect

Diff for: go.sum

+12-2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EU
6969
contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
7070
contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
7171
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
72+
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2 h1:2OrsyJYZp7J6nyAsKi2q1SELYRaIc0aQmcQ/EQqPfk8=
73+
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20200411073322-f0bcc40f0bf2/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
7274
gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb h1:Yy0Bxzc8R2wxiwXoG/rECGplJUSpXqCsog9PuJFgiHs=
7375
gitea.com/go-chi/binding v0.0.0-20220309004920-114340dabecb/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc=
7476
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:xkRDCp4j0
460462
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
461463
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
462464
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
465+
github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b h1:+RjYfEfoZdM3wHFs752dlOpGaoRhwRRyQxjajg08LcQ=
466+
github.com/go-ap/activitypub v0.0.0-20220615144428-48208c70483b/go.mod h1:DE3vvc6Didgfd3k7M1Mos6qMDFNmMrxJmYVMHG9h9Io=
467+
github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f h1:kJhGo4NApJP0Lt9lkJnfmuTnRWVFbCynY0kiTxpPUR4=
468+
github.com/go-ap/errors v0.0.0-20220615144307-e8bc4a40ae9f/go.mod h1:KHkKFKZvc05lr79+RGoq/zG8YjWi3+FK60Bxd+mpCew=
469+
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d h1:Z/oRXMlZHjvjIqDma1FrIGL3iE5YL7MUI0bwYEZ6qbA=
470+
github.com/go-ap/jsonld v0.0.0-20220615144122-1d862b15410d/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
463471
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
464472
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
465473
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/6DOe2AOJEOihJdqda83nPyJNtMR
472480
github.com/go-enry/go-enry/v2 v2.8.2/go.mod h1:GVzIiAytiS5uT/QiuakK7TF1u4xDab87Y8V5EJRpsIQ=
473481
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
474482
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
475-
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
476-
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
483+
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=
484+
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
477485
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
478486
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
479487
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=
15071515
github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
15081516
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
15091517
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
1518+
github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
1519+
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
15101520
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
15111521
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
15121522
github.com/weppos/publicsuffix-go v0.13.1-0.20210123135404-5fd73613514e/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE=

Diff for: integrations/api_activitypub_person_test.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"net/http"
11+
"net/http/httptest"
12+
"net/url"
13+
"testing"
14+
15+
user_model "code.gitea.io/gitea/models/user"
16+
"code.gitea.io/gitea/modules/activitypub"
17+
"code.gitea.io/gitea/modules/setting"
18+
19+
ap "github.com/go-ap/activitypub"
20+
"github.com/stretchr/testify/assert"
21+
)
22+
23+
func TestActivityPubPerson(t *testing.T) {
24+
onGiteaRun(t, func(*testing.T, *url.URL) {
25+
setting.Federation.Enabled = true
26+
defer func() {
27+
setting.Federation.Enabled = false
28+
}()
29+
30+
username := "user2"
31+
req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username))
32+
resp := MakeRequest(t, req, http.StatusOK)
33+
body := resp.Body.Bytes()
34+
assert.Contains(t, string(body), "@context")
35+
36+
var person ap.Person
37+
err := person.UnmarshalJSON(body)
38+
assert.NoError(t, err)
39+
40+
assert.Equal(t, ap.PersonType, person.Type)
41+
assert.Equal(t, username, person.PreferredUsername.String())
42+
keyID := person.GetID().String()
43+
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyID)
44+
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.Outbox.GetID().String())
45+
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.Inbox.GetID().String())
46+
47+
pubKey := person.PublicKey
48+
assert.NotNil(t, pubKey)
49+
publicKeyID := keyID + "#main-key"
50+
assert.Equal(t, pubKey.ID.String(), publicKeyID)
51+
52+
pubKeyPem := pubKey.PublicKeyPem
53+
assert.NotNil(t, pubKeyPem)
54+
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
55+
})
56+
}
57+
58+
func TestActivityPubMissingPerson(t *testing.T) {
59+
onGiteaRun(t, func(*testing.T, *url.URL) {
60+
setting.Federation.Enabled = true
61+
defer func() {
62+
setting.Federation.Enabled = false
63+
}()
64+
65+
req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser")
66+
resp := MakeRequest(t, req, http.StatusNotFound)
67+
assert.Contains(t, resp.Body.String(), "user redirect does not exist")
68+
})
69+
}
70+
71+
func TestActivityPubPersonInbox(t *testing.T) {
72+
srv := httptest.NewServer(c)
73+
defer srv.Close()
74+
75+
onGiteaRun(t, func(*testing.T, *url.URL) {
76+
appURL := setting.AppURL
77+
setting.Federation.Enabled = true
78+
setting.AppURL = srv.URL
79+
defer func() {
80+
setting.Federation.Enabled = false
81+
setting.Database.LogSQL = false
82+
setting.AppURL = appURL
83+
}()
84+
username1 := "user1"
85+
ctx := context.Background()
86+
user1, err := user_model.GetUserByName(ctx, username1)
87+
assert.NoError(t, err)
88+
user1url := fmt.Sprintf("%s/api/v1/activitypub/user/%s#main-key", srv.URL, username1)
89+
c, err := activitypub.NewClient(user1, user1url)
90+
assert.NoError(t, err)
91+
username2 := "user2"
92+
user2inboxurl := fmt.Sprintf("%s/api/v1/activitypub/user/%s/inbox", srv.URL, username2)
93+
94+
// Signed request succeeds
95+
resp, err := c.Post([]byte{}, user2inboxurl)
96+
assert.NoError(t, err)
97+
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
98+
99+
// Unsigned request fails
100+
req := NewRequest(t, "POST", user2inboxurl)
101+
MakeRequest(t, req, http.StatusInternalServerError)
102+
})
103+
}

Diff for: integrations/webfinger_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func TestWebfinger(t *testing.T) {
5252
var jrd webfingerJRD
5353
DecodeJSON(t, resp, &jrd)
5454
assert.Equal(t, "acct:user2@"+appURL.Host, jrd.Subject)
55-
assert.ElementsMatch(t, []string{user.HTMLURL()}, jrd.Aliases)
55+
assert.ElementsMatch(t, []string{user.HTMLURL(), appURL.String() + "api/v1/activitypub/user/" + url.PathEscape(user.Name)}, jrd.Aliases)
5656

5757
req = NewRequest(t, "GET", fmt.Sprintf("/.well-known/webfinger?resource=acct:%s@%s", user.LowerName, "unknown.host"))
5858
MakeRequest(t, req, http.StatusBadRequest)

Diff for: models/user/setting_keys.go

+4
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ const (
99
SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types"
1010
// SettingsKeyDiffWhitespaceBehavior is the setting key for whitespace behavior of diff
1111
SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour"
12+
// UserActivityPubPrivPem is user's private key
13+
UserActivityPubPrivPem = "activitypub.priv_pem"
14+
// UserActivityPubPubPem is user's public key
15+
UserActivityPubPubPem = "activitypub.pub_pem"
1216
)

Diff for: modules/activitypub/client.go

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2022 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package activitypub
6+
7+
import (
8+
"bytes"
9+
"crypto/rsa"
10+
"crypto/x509"
11+
"encoding/pem"
12+
"fmt"
13+
"net/http"
14+
"strings"
15+
"time"
16+
17+
user_model "code.gitea.io/gitea/models/user"
18+
"code.gitea.io/gitea/modules/proxy"
19+
"code.gitea.io/gitea/modules/setting"
20+
21+
"github.com/go-fed/httpsig"
22+
)
23+
24+
const (
25+
// ActivityStreamsContentType const
26+
ActivityStreamsContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
27+
httpsigExpirationTime = 60
28+
)
29+
30+
// Gets the current time as an RFC 2616 formatted string
31+
// RFC 2616 requires RFC 1123 dates but with GMT instead of UTC
32+
func CurrentTime() string {
33+
return strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT")
34+
}
35+
36+
func containsRequiredHTTPHeaders(method string, headers []string) error {
37+
var hasRequestTarget, hasDate, hasDigest bool
38+
for _, header := range headers {
39+
hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget
40+
hasDate = hasDate || header == "Date"
41+
hasDigest = hasDigest || header == "Digest"
42+
}
43+
if !hasRequestTarget {
44+
return fmt.Errorf("missing http header for %s: %s", method, httpsig.RequestTarget)
45+
} else if !hasDate {
46+
return fmt.Errorf("missing http header for %s: Date", method)
47+
} else if !hasDigest && method != http.MethodGet {
48+
return fmt.Errorf("missing http header for %s: Digest", method)
49+
}
50+
return nil
51+
}
52+
53+
// Client struct
54+
type Client struct {
55+
client *http.Client
56+
algs []httpsig.Algorithm
57+
digestAlg httpsig.DigestAlgorithm
58+
getHeaders []string
59+
postHeaders []string
60+
priv *rsa.PrivateKey
61+
pubID string
62+
}
63+
64+
// NewClient function
65+
func NewClient(user *user_model.User, pubID string) (c *Client, err error) {
66+
if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil {
67+
return
68+
} else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil {
69+
return
70+
}
71+
72+
priv, err := GetPrivateKey(user)
73+
if err != nil {
74+
return
75+
}
76+
privPem, _ := pem.Decode([]byte(priv))
77+
privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
78+
if err != nil {
79+
return
80+
}
81+
82+
c = &Client{
83+
client: &http.Client{
84+
Transport: &http.Transport{
85+
Proxy: proxy.Proxy(),
86+
},
87+
},
88+
algs: setting.HttpsigAlgs,
89+
digestAlg: httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm),
90+
getHeaders: setting.Federation.GetHeaders,
91+
postHeaders: setting.Federation.PostHeaders,
92+
priv: privParsed,
93+
pubID: pubID,
94+
}
95+
return
96+
}
97+
98+
// NewRequest function
99+
func (c *Client) NewRequest(b []byte, to string) (req *http.Request, err error) {
100+
buf := bytes.NewBuffer(b)
101+
req, err = http.NewRequest(http.MethodPost, to, buf)
102+
if err != nil {
103+
return
104+
}
105+
req.Header.Add("Content-Type", ActivityStreamsContentType)
106+
req.Header.Add("Date", CurrentTime())
107+
req.Header.Add("User-Agent", "Gitea/"+setting.AppVer)
108+
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime)
109+
if err != nil {
110+
return
111+
}
112+
err = signer.SignRequest(c.priv, c.pubID, req, b)
113+
return
114+
}
115+
116+
// Post function
117+
func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
118+
var req *http.Request
119+
if req, err = c.NewRequest(b, to); err != nil {
120+
return
121+
}
122+
resp, err = c.client.Do(req)
123+
return
124+
}

0 commit comments

Comments
 (0)