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  }