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