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{}