code.gitea.io/gitea@v1.22.3/modules/activitypub/client.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package activitypub 5 6 import ( 7 "bytes" 8 "context" 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(ctx context.Context, user *user_model.User, pubID string) (c *Client, err error) { 66 if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil { 67 return nil, err 68 } else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil { 69 return nil, err 70 } 71 72 priv, err := GetPrivateKey(ctx, user) 73 if err != nil { 74 return nil, err 75 } 76 privPem, _ := pem.Decode([]byte(priv)) 77 privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) 78 if err != nil { 79 return nil, err 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 c, err 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 nil, err 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 nil, err 111 } 112 err = signer.SignRequest(c.priv, c.pubID, req, b) 113 return req, err 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 nil, err 121 } 122 resp, err = c.client.Do(req) 123 return resp, err 124 }