mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 03:14:29 +01:00 
			
		
		
		
	activitypub: signing http client
Signed-off-by: Loïc Dachary <loic@dachary.org>
This commit is contained in:
		
							parent
							
								
									e8907c3c9e
								
							
						
					
					
						commit
						15c1f6218c
					
				
							
								
								
									
										117
									
								
								modules/activitypub/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								modules/activitypub/client.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,117 @@ | ||||
| // Copyright 2021 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" | ||||
| 
 | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"github.com/go-fed/activity/pub" | ||||
| 	"github.com/go-fed/httpsig" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	activityStreamsContentType = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" | ||||
| ) | ||||
| 
 | ||||
| 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 = method == "GET" || 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 { | ||||
| 		return fmt.Errorf("missing http header for %s: Digest", method) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type Client struct { | ||||
| 	clock       pub.Clock | ||||
| 	client      *http.Client | ||||
| 	algs        []httpsig.Algorithm | ||||
| 	digestAlg   httpsig.DigestAlgorithm | ||||
| 	getHeaders  []string | ||||
| 	postHeaders []string | ||||
| 	priv        *rsa.PrivateKey | ||||
| 	pubId       string | ||||
| } | ||||
| 
 | ||||
| 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 | ||||
| 	} else if !httpsig.IsSupportedDigestAlgorithm(setting.Federation.DigestAlgorithm) { | ||||
| 		err = fmt.Errorf("unsupported digest algorithm: %s", setting.Federation.DigestAlgorithm) | ||||
| 		return | ||||
| 	} | ||||
| 	algos := make([]httpsig.Algorithm, len(setting.Federation.Algorithms)) | ||||
| 	for i, algo := range setting.Federation.Algorithms { | ||||
| 		algos[i] = httpsig.Algorithm(algo) | ||||
| 	} | ||||
| 	clock, err := NewClock() | ||||
| 	if 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{ | ||||
| 		clock:       clock, | ||||
| 		client:      &http.Client{}, | ||||
| 		algs:        algos, | ||||
| 		digestAlg:   httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm), | ||||
| 		getHeaders:  setting.Federation.GetHeaders, | ||||
| 		postHeaders: setting.Federation.PostHeaders, | ||||
| 		priv:        privParsed, | ||||
| 		pubId:       pubId, | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) { | ||||
| 	byteCopy := make([]byte, len(b)) | ||||
| 	copy(byteCopy, b) | ||||
| 	buf := bytes.NewBuffer(byteCopy) | ||||
| 	var req *http.Request | ||||
| 	req, err = http.NewRequest(http.MethodPost, to, buf) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	req.Header.Add("Content-Type", activityStreamsContentType) | ||||
| 	req.Header.Add("Accept-Charset", "utf-8") | ||||
| 	req.Header.Add("Date", fmt.Sprintf("%s GMT", c.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05"))) | ||||
| 
 | ||||
| 	signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, 60) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	err = signer.SignRequest(c.priv, c.pubId, req, b) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	resp, err = c.client.Do(req) | ||||
| 	return | ||||
| } | ||||
							
								
								
									
										49
									
								
								modules/activitypub/client_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								modules/activitypub/client_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | ||||
| // Copyright 2021 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" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"regexp" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	_ "code.gitea.io/gitea/models" // https://discourse.gitea.io/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4 | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 
 | ||||
| 	"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 := ioutil.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)) | ||||
| } | ||||
							
								
								
									
										16
									
								
								modules/activitypub/main_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								modules/activitypub/main_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| // Copyright 2021 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, filepath.Join("..", "..")) | ||||
| } | ||||
| @ -10,8 +10,16 @@ import "code.gitea.io/gitea/modules/log" | ||||
| var ( | ||||
| 	Federation = struct { | ||||
| 		Enabled         bool | ||||
| 		Algorithms      []string | ||||
| 		DigestAlgorithm string | ||||
| 		GetHeaders      []string | ||||
| 		PostHeaders     []string | ||||
| 	}{ | ||||
| 		Enabled:         true, | ||||
| 		Algorithms:      []string{"rsa-sha256", "rsa-sha512"}, | ||||
| 		DigestAlgorithm: "SHA-256", | ||||
| 		GetHeaders:      []string{"(request-target)", "Date"}, | ||||
| 		PostHeaders:     []string{"(request-target)", "Date", "Digest"}, | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user