gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

transport.go (6449B)


      1 package pub
      2 
      3 import (
      4 	"bytes"
      5 	"context"
      6 	"crypto"
      7 	"fmt"
      8 	"io/ioutil"
      9 	"net/http"
     10 	"net/url"
     11 	"strings"
     12 	"sync"
     13 
     14 	"github.com/go-fed/httpsig"
     15 )
     16 
     17 const (
     18 	// acceptHeaderValue is the Accept header value indicating that the
     19 	// response should contain an ActivityStreams object.
     20 	acceptHeaderValue = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
     21 )
     22 
     23 // isSuccess returns true if the HTTP status code is either OK, Created, or
     24 // Accepted.
     25 func isSuccess(code int) bool {
     26 	return code == http.StatusOK ||
     27 		code == http.StatusCreated ||
     28 		code == http.StatusAccepted
     29 }
     30 
     31 // Transport makes ActivityStreams calls to other servers in order to send or
     32 // receive ActivityStreams data.
     33 //
     34 // It is responsible for setting the appropriate request headers, signing the
     35 // requests if needed, and facilitating the traffic between this server and
     36 // another.
     37 //
     38 // The transport is exclusively used to issue requests on behalf of an actor,
     39 // and is never sending requests on behalf of the server in general.
     40 //
     41 // It may be reused multiple times, but never concurrently.
     42 type Transport interface {
     43 	// Dereference fetches the ActivityStreams object located at this IRI
     44 	// with a GET request.
     45 	Dereference(c context.Context, iri *url.URL) ([]byte, error)
     46 	// Deliver sends an ActivityStreams object.
     47 	Deliver(c context.Context, b []byte, to *url.URL) error
     48 	// BatchDeliver sends an ActivityStreams object to multiple recipients.
     49 	BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error
     50 }
     51 
     52 // Transport must be implemented by HttpSigTransport.
     53 var _ Transport = &HttpSigTransport{}
     54 
     55 // HttpSigTransport makes a dereference call using HTTP signatures to
     56 // authenticate the request on behalf of a particular actor.
     57 //
     58 // No rate limiting is applied.
     59 //
     60 // Only one request is tried per call.
     61 type HttpSigTransport struct {
     62 	client       HttpClient
     63 	appAgent     string
     64 	gofedAgent   string
     65 	clock        Clock
     66 	getSigner    httpsig.Signer
     67 	getSignerMu  *sync.Mutex
     68 	postSigner   httpsig.Signer
     69 	postSignerMu *sync.Mutex
     70 	pubKeyId     string
     71 	privKey      crypto.PrivateKey
     72 }
     73 
     74 // NewHttpSigTransport returns a new Transport.
     75 //
     76 // It sends requests specifically on behalf of a specific actor on this server.
     77 // The actor's credentials are used to add an HTTP Signature to requests, which
     78 // requires an actor's private key, a unique identifier for their public key,
     79 // and an HTTP Signature signing algorithm.
     80 //
     81 // The client lets users issue requests through any HTTP client, including the
     82 // standard library's HTTP client.
     83 //
     84 // The appAgent uniquely identifies the calling application's requests, so peers
     85 // may aid debugging the requests incoming from this server. Note that the
     86 // agent string will also include one for go-fed, so at minimum peer servers can
     87 // reach out to the go-fed library to aid in notifying implementors of malformed
     88 // or unsupported requests.
     89 func NewHttpSigTransport(
     90 	client HttpClient,
     91 	appAgent string,
     92 	clock Clock,
     93 	getSigner, postSigner httpsig.Signer,
     94 	pubKeyId string,
     95 	privKey crypto.PrivateKey) *HttpSigTransport {
     96 	return &HttpSigTransport{
     97 		client:       client,
     98 		appAgent:     appAgent,
     99 		gofedAgent:   goFedUserAgent(),
    100 		clock:        clock,
    101 		getSigner:    getSigner,
    102 		getSignerMu:  &sync.Mutex{},
    103 		postSigner:   postSigner,
    104 		postSignerMu: &sync.Mutex{},
    105 		pubKeyId:     pubKeyId,
    106 		privKey:      privKey,
    107 	}
    108 }
    109 
    110 // Dereference sends a GET request signed with an HTTP Signature to obtain an
    111 // ActivityStreams value.
    112 func (h HttpSigTransport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
    113 	req, err := http.NewRequest("GET", iri.String(), nil)
    114 	if err != nil {
    115 		return nil, err
    116 	}
    117 	req = req.WithContext(c)
    118 	req.Header.Add(acceptHeader, acceptHeaderValue)
    119 	req.Header.Add("Accept-Charset", "utf-8")
    120 	req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
    121 	req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent))
    122 	req.Header.Set("Host", iri.Host)
    123 	h.getSignerMu.Lock()
    124 	err = h.getSigner.SignRequest(h.privKey, h.pubKeyId, req, nil)
    125 	h.getSignerMu.Unlock()
    126 	if err != nil {
    127 		return nil, err
    128 	}
    129 	resp, err := h.client.Do(req)
    130 	if err != nil {
    131 		return nil, err
    132 	}
    133 	defer resp.Body.Close()
    134 	if resp.StatusCode != http.StatusOK {
    135 		return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
    136 	}
    137 	return ioutil.ReadAll(resp.Body)
    138 }
    139 
    140 // Deliver sends a POST request with an HTTP Signature.
    141 func (h HttpSigTransport) Deliver(c context.Context, b []byte, to *url.URL) error {
    142 	req, err := http.NewRequest("POST", to.String(), bytes.NewReader(b))
    143 	if err != nil {
    144 		return err
    145 	}
    146 	req = req.WithContext(c)
    147 	req.Header.Add(contentTypeHeader, contentTypeHeaderValue)
    148 	req.Header.Add("Accept-Charset", "utf-8")
    149 	req.Header.Add("Date", h.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
    150 	req.Header.Add("User-Agent", fmt.Sprintf("%s %s", h.appAgent, h.gofedAgent))
    151 	req.Header.Set("Host", to.Host)
    152 	h.postSignerMu.Lock()
    153 	err = h.postSigner.SignRequest(h.privKey, h.pubKeyId, req, b)
    154 	h.postSignerMu.Unlock()
    155 	if err != nil {
    156 		return err
    157 	}
    158 	resp, err := h.client.Do(req)
    159 	if err != nil {
    160 		return err
    161 	}
    162 	defer resp.Body.Close()
    163 	if !isSuccess(resp.StatusCode) {
    164 		return fmt.Errorf("POST request to %s failed (%d): %s", to.String(), resp.StatusCode, resp.Status)
    165 	}
    166 	return nil
    167 }
    168 
    169 // BatchDeliver sends concurrent POST requests. Returns an error if any of the
    170 // requests had an error.
    171 func (h HttpSigTransport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
    172 	var wg sync.WaitGroup
    173 	errCh := make(chan error, len(recipients))
    174 	for _, recipient := range recipients {
    175 		wg.Add(1)
    176 		go func(r *url.URL) {
    177 			defer wg.Done()
    178 			if err := h.Deliver(c, b, r); err != nil {
    179 				errCh <- err
    180 			}
    181 		}(recipient)
    182 	}
    183 	wg.Wait()
    184 	errs := make([]string, 0, len(recipients))
    185 outer:
    186 	for {
    187 		select {
    188 		case e := <-errCh:
    189 			errs = append(errs, e.Error())
    190 		default:
    191 			break outer
    192 		}
    193 	}
    194 	if len(errs) > 0 {
    195 		return fmt.Errorf("batch deliver had at least one failure: %s", strings.Join(errs, "; "))
    196 	}
    197 	return nil
    198 }
    199 
    200 // HttpClient sends http requests, and is an abstraction only needed by the
    201 // HttpSigTransport. The standard library's Client satisfies this interface.
    202 type HttpClient interface {
    203 	Do(req *http.Request) (*http.Response, error)
    204 }
    205 
    206 // HttpClient must be implemented by http.Client.
    207 var _ HttpClient = &http.Client{}