Compare commits

..

5 Commits

11 changed files with 54 additions and 304 deletions

View File

@ -1,19 +0,0 @@
---
kind: pipeline
name: automated container publishing
steps:
- name: publish image
image: plugins/docker
settings:
username:
from_secret: docker_reg_username
password:
from_secret: docker_reg_passwd
repo: decentral1se/gotosocial
tags: latest
trigger:
branch:
- main
event:
exclude:
- pull_request

View File

@ -2,8 +2,6 @@
![patrons](https://img.shields.io/liberapay/patrons/dumpsterqueer.svg?logo=liberapay) ![receives](https://img.shields.io/liberapay/receives/dumpsterqueer.svg?logo=liberapay) ![patrons](https://img.shields.io/liberapay/patrons/dumpsterqueer.svg?logo=liberapay) ![receives](https://img.shields.io/liberapay/receives/dumpsterqueer.svg?logo=liberapay)
[![Build Status](https://drone.autonomic.zone/api/badges/autonomic-cooperative/gotosocial/status.svg?ref=refs/heads/main)](https://drone.autonomic.zone/autonomic-cooperative/gotosocial)
Federated social media software. Federated social media software.
![Sloth logo made by Freepik from www.flaticon.com](./web/assets/sloth.png) ![Sloth logo made by Freepik from www.flaticon.com](./web/assets/sloth.png)

View File

@ -26,6 +26,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/mail" "net/mail"
"regexp"
"strings" "strings"
"time" "time"
@ -47,6 +48,7 @@ type postgresService struct {
conn *pg.DB conn *pg.DB
log *logrus.Logger log *logrus.Logger
cancel context.CancelFunc cancel context.CancelFunc
// federationDB pub.Database
} }
// NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface. // NewPostgresService returns a postgresService derived from the provided config, which implements the go-fed DB interface.
@ -118,6 +120,12 @@ func derivePGOptions(c *config.Config) (*pg.Options, error) {
return nil, errors.New("no address set") return nil, errors.New("no address set")
} }
ipv4Regex := regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`)
hostnameRegex := regexp.MustCompile(`^(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]{2,}$`)
if !hostnameRegex.MatchString(c.DBConfig.Address) && !ipv4Regex.MatchString(c.DBConfig.Address) && c.DBConfig.Address != "localhost" {
return nil, fmt.Errorf("address %s was neither an ipv4 address nor a valid hostname", c.DBConfig.Address)
}
// validate username // validate username
if c.DBConfig.User == "" { if c.DBConfig.User == "" {
return nil, errors.New("no user set") return nil, errors.New("no user set")

View File

@ -14,8 +14,6 @@ import (
) )
func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error) { func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (typeutils.Accountable, error) {
f.startHandshake(username, remoteAccountID)
defer f.stopHandshake(username, remoteAccountID)
transport, err := f.GetTransportForUser(username) transport, err := f.GetTransportForUser(username)
if err != nil { if err != nil {

View File

@ -21,7 +21,6 @@ package federation
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"sync"
"github.com/go-fed/activity/pub" "github.com/go-fed/activity/pub"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -59,8 +58,6 @@ type Federator interface {
// //
// If username is an empty string, our instance user's credentials will be used instead. // If username is an empty string, our instance user's credentials will be used instead.
GetTransportForUser(username string) (transport.Transport, error) GetTransportForUser(username string) (transport.Transport, error)
// Handshaking returns true if the given username is currently in the process of dereferencing the remoteAccountID.
Handshaking(username string, remoteAccountID *url.URL) bool
pub.CommonBehavior pub.CommonBehavior
pub.FederatingProtocol pub.FederatingProtocol
} }
@ -74,8 +71,6 @@ type federator struct {
transportController transport.Controller transportController transport.Controller
actor pub.FederatingActor actor pub.FederatingActor
log *logrus.Logger log *logrus.Logger
handshakes map[string][]*url.URL
handshakeSync *sync.Mutex // mutex to lock/unlock when checking or updating the handshakes map
} }
// NewFederator returns a new federator // NewFederator returns a new federator
@ -90,7 +85,6 @@ func NewFederator(db db.DB, federatingDB federatingdb.DB, transportController tr
typeConverter: typeConverter, typeConverter: typeConverter,
transportController: transportController, transportController: transportController,
log: log, log: log,
handshakeSync: &sync.Mutex{},
} }
actor := newFederatingActor(f, f, federatingDB, clock) actor := newFederatingActor(f, f, federatingDB, clock)
f.actor = actor f.actor = actor

View File

@ -1,80 +0,0 @@
package federation
import "net/url"
func (f *federator) Handshaking(username string, remoteAccountID *url.URL) bool {
f.handshakeSync.Lock()
defer f.handshakeSync.Unlock()
if f.handshakes == nil {
// handshakes isn't even initialized yet so we can't be handshaking with anyone
return false
}
remoteIDs, ok := f.handshakes[username]
if !ok {
// user isn't handshaking with anyone, bail
return false
}
for _, id := range remoteIDs {
if id.String() == remoteAccountID.String() {
// we are currently handshaking with the remote account, yep
return true
}
}
// didn't find it which means we're not handshaking
return false
}
func (f *federator) startHandshake(username string, remoteAccountID *url.URL) {
f.handshakeSync.Lock()
defer f.handshakeSync.Unlock()
// lazily initialize handshakes
if f.handshakes == nil {
f.handshakes = make(map[string][]*url.URL)
}
remoteIDs, ok := f.handshakes[username]
if !ok {
// there was nothing in there yet, so just add this entry and return
f.handshakes[username] = []*url.URL{remoteAccountID}
return
}
// add the remote ID to the slice
remoteIDs = append(remoteIDs, remoteAccountID)
f.handshakes[username] = remoteIDs
}
func (f *federator) stopHandshake(username string, remoteAccountID *url.URL) {
f.handshakeSync.Lock()
defer f.handshakeSync.Unlock()
if f.handshakes == nil {
return
}
remoteIDs, ok := f.handshakes[username]
if !ok {
// there was nothing in there yet anyway so just bail
return
}
newRemoteIDs := []*url.URL{}
for _, id := range remoteIDs {
if id.String() != remoteAccountID.String() {
newRemoteIDs = append(newRemoteIDs, id)
}
}
if len(newRemoteIDs) == 0 {
// there are no handshakes so just remove this user entry from the map and save a few bytes
delete(f.handshakes, username)
} else {
// there are still other handshakes ongoing
f.handshakes[username] = newRemoteIDs
}
}

View File

@ -26,6 +26,7 @@ import (
"github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
@ -34,16 +35,23 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
// dereferenceFediRequest authenticates the HTTP signature of an incoming federation request, using the given // authenticateAndDereferenceFediRequest authenticates the HTTP signature of an incoming federation request, using the given
// username to perform the validation. It will *also* dereference the originator of the request and return it as a gtsmodel account // username to perform the validation. It will *also* dereference the originator of the request and return it as a gtsmodel account
// for further processing. NOTE that this function will have the side effect of putting the dereferenced account into the database, // for further processing. NOTE that this function will have the side effect of putting the dereferenced account into the database,
// and passing it into the processor through a channel for further asynchronous processing. // and passing it into the processor through a channel for further asynchronous processing.
func (p *processor) dereferenceFediRequest(username string, requestingAccountURI *url.URL) (*gtsmodel.Account, error) { func (p *processor) authenticateAndDereferenceFediRequest(username string, r *http.Request) (*gtsmodel.Account, error) {
// first authenticate
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(username, r)
if err != nil {
return nil, fmt.Errorf("couldn't authenticate request for username %s: %s", username, err)
}
// OK now we can do the dereferencing part // OK now we can do the dereferencing part
// we might already have an entry for this account so check that first // we might already have an entry for this account so check that first
requestingAccount := &gtsmodel.Account{} requestingAccount := &gtsmodel.Account{}
err := p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount) err = p.db.GetWhere([]db.Where{{Key: "uri", Value: requestingAccountURI.String()}}, requestingAccount)
if err == nil { if err == nil {
// we do have it yay, return it // we do have it yay, return it
return requestingAccount, nil return requestingAccount, nil
@ -90,6 +98,12 @@ func (p *processor) dereferenceFediRequest(username string, requestingAccountURI
} }
func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) { func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
l := p.log.WithFields(logrus.Fields{
"func": "GetFediUser",
"requestedUsername": requestedUsername,
"requestURL": request.URL.String(),
})
// get the account the request is referring to // get the account the request is referring to
requestedAccount := &gtsmodel.Account{} requestedAccount := &gtsmodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@ -99,21 +113,16 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
var requestedPerson vocab.ActivityStreamsPerson var requestedPerson vocab.ActivityStreamsPerson
var err error var err error
if util.IsPublicKeyPath(request.URL) { if util.IsPublicKeyPath(request.URL) {
l.Debug("serving from public key path")
// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount) requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
} }
} else if util.IsUserPath(request.URL) { } else if util.IsUserPath(request.URL) {
l.Debug("serving from user path")
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile // if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
}
// if we're already handshaking/dereferencing a remote account, we can skip the dereferencing part
if !p.federator.Handshaking(requestedUsername, requestingAccountURI) {
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
if err != nil { if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err) return nil, gtserror.NewErrorNotAuthorized(err)
} }
@ -126,8 +135,6 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
if blocked { if blocked {
return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
} }
}
requestedPerson, err = p.tc.AccountToAS(requestedAccount) requestedPerson, err = p.tc.AccountToAS(requestedAccount)
if err != nil { if err != nil {
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)
@ -152,12 +159,7 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
} }
// authenticate the request // authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
if err != nil { if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err) return nil, gtserror.NewErrorNotAuthorized(err)
} }
@ -197,12 +199,7 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
} }
// authenticate the request // authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
if err != nil { if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err) return nil, gtserror.NewErrorNotAuthorized(err)
} }
@ -242,12 +239,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
} }
// authenticate the request // authenticate the request
requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request) requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err)
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
if err != nil { if err != nil {
return nil, gtserror.NewErrorNotAuthorized(err) return nil, gtserror.NewErrorNotAuthorized(err)
} }

View File

@ -13,7 +13,6 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/util"
) )
func (t *transport) DereferenceInstance(c context.Context, iri *url.URL) (*gtsmodel.Instance, error) { func (t *transport) DereferenceInstance(c context.Context, iri *url.URL) (*gtsmodel.Instance, error) {
@ -27,7 +26,7 @@ func (t *transport) DereferenceInstance(c context.Context, iri *url.URL) (*gtsmo
// //
// This will only work with Mastodon-api compatible instances: Mastodon, some Pleroma instances, GoToSocial. // This will only work with Mastodon-api compatible instances: Mastodon, some Pleroma instances, GoToSocial.
l.Debugf("trying to dereference instance %s by /api/v1/instance", iri.Host) l.Debugf("trying to dereference instance %s by /api/v1/instance", iri.Host)
i, err = dereferenceByAPIV1Instance(c, t, iri) i, err = dereferenceByAPIV1Instance(t, c, iri)
if err == nil { if err == nil {
l.Debugf("successfully dereferenced instance using /api/v1/instance") l.Debugf("successfully dereferenced instance using /api/v1/instance")
return i, nil return i, nil
@ -37,28 +36,17 @@ func (t *transport) DereferenceInstance(c context.Context, iri *url.URL) (*gtsmo
// If that doesn't work, try to dereference using /.well-known/nodeinfo. // If that doesn't work, try to dereference using /.well-known/nodeinfo.
// This will involve two API calls and return less info overall, but should be more widely compatible. // This will involve two API calls and return less info overall, but should be more widely compatible.
l.Debugf("trying to dereference instance %s by /.well-known/nodeinfo", iri.Host) l.Debugf("trying to dereference instance %s by /.well-known/nodeinfo", iri.Host)
i, err = dereferenceByNodeInfo(c, t, iri) i, err = dereferenceByNodeInfo(t, c, iri)
if err == nil { if err == nil {
l.Debugf("successfully dereferenced instance using /.well-known/nodeinfo") l.Debugf("successfully dereferenced instance using /.well-known/nodeinfo")
return i, nil return i, nil
} }
l.Debugf("couldn't dereference instance using /.well-known/nodeinfo: %s", err) l.Debugf("couldn't dereference instance using /.well-known/nodeinfo: %s", err)
// we couldn't dereference the instance using any of the known methods, so just return a minimal representation return nil, fmt.Errorf("couldn't dereference instance %s using either /api/v1/instance or /.well-known/nodeinfo", iri.Host)
l.Debugf("returning minimal representation of instance %s", iri.Host)
id, err := id.NewRandomULID()
if err != nil {
return nil, fmt.Errorf("error creating new id for instance %s: %s", iri.Host, err)
}
return &gtsmodel.Instance{
ID: id,
Domain: iri.Host,
URI: iri.String(),
}, nil
} }
func dereferenceByAPIV1Instance(c context.Context, t *transport, iri *url.URL) (*gtsmodel.Instance, error) { func dereferenceByAPIV1Instance(t *transport, c context.Context, iri *url.URL) (*gtsmodel.Instance, error) {
l := t.log.WithField("func", "dereferenceByAPIV1Instance") l := t.log.WithField("func", "dereferenceByAPIV1Instance")
cleanIRI := &url.URL{ cleanIRI := &url.URL{
@ -131,93 +119,8 @@ func dereferenceByAPIV1Instance(c context.Context, t *transport, iri *url.URL) (
return i, nil return i, nil
} }
func dereferenceByNodeInfo(c context.Context, t *transport, iri *url.URL) (*gtsmodel.Instance, error) { func dereferenceByNodeInfo(t *transport, c context.Context, iri *url.URL) (*gtsmodel.Instance, error) {
niIRI, err := callNodeInfoWellKnown(c, t, iri) l := t.log.WithField("func", "dereferenceByNodeInfo")
if err != nil {
return nil, fmt.Errorf("dereferenceByNodeInfo: error during initial call to well-known nodeinfo: %s", err)
}
ni, err := callNodeInfo(c, t, niIRI)
if err != nil {
return nil, fmt.Errorf("dereferenceByNodeInfo: error doing second call to nodeinfo uri %s: %s", niIRI.String(), err)
}
// we got a response of some kind! take what we can from it...
id, err := id.NewRandomULID()
if err != nil {
return nil, fmt.Errorf("dereferenceByNodeInfo: error creating new id for instance %s: %s", iri.Host, err)
}
// this is the bare minimum instance we'll return, and we'll add more stuff to it if we can
i := &gtsmodel.Instance{
ID: id,
Domain: iri.Host,
URI: iri.String(),
}
var title string
if i, present := ni.Metadata["nodeName"]; present {
// it's present, check it's a string
if v, ok := i.(string); ok {
// it is a string!
title = v
}
}
i.Title = title
var shortDescription string
if i, present := ni.Metadata["nodeDescription"]; present {
// it's present, check it's a string
if v, ok := i.(string); ok {
// it is a string!
shortDescription = v
}
}
i.ShortDescription = shortDescription
var contactEmail string
var contactAccountUsername string
if i, present := ni.Metadata["maintainer"]; present {
// it's present, check it's a map
if v, ok := i.(map[string]string); ok {
// see if there's an email in the map
if email, present := v["email"]; present {
if err := util.ValidateEmail(email); err == nil {
// valid email address
contactEmail = email
}
}
// see if there's a 'name' in the map
if name, present := v["name"]; present {
// name could be just a username, or could be a mention string eg @whatever@aaaa.com
username, _, err := util.ExtractMentionParts(name)
if err == nil {
// it was a mention string
contactAccountUsername = username
} else {
// not a mention string
contactAccountUsername = name
}
}
}
}
i.ContactEmail = contactEmail
i.ContactAccountUsername = contactAccountUsername
var software string
if ni.Software.Name != "" {
software = ni.Software.Name
}
if ni.Software.Version != "" {
software = software + " " + ni.Software.Version
}
i.Version = software
return i, nil
}
func callNodeInfoWellKnown(c context.Context, t *transport, iri *url.URL) (*url.URL, error) {
l := t.log.WithField("func", "callNodeInfoWellKnown")
cleanIRI := &url.URL{ cleanIRI := &url.URL{
Scheme: iri.Scheme, Scheme: iri.Scheme,
@ -247,7 +150,7 @@ func callNodeInfoWellKnown(c context.Context, t *transport, iri *url.URL) (*url.
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("callNodeInfoWellKnown: GET request to %s failed (%d): %s", cleanIRI.String(), resp.StatusCode, resp.Status) return nil, fmt.Errorf("GET request to %s failed (%d): %s", cleanIRI.String(), resp.StatusCode, resp.Status)
} }
b, err := ioutil.ReadAll(resp.Body) b, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
@ -255,72 +158,28 @@ func callNodeInfoWellKnown(c context.Context, t *transport, iri *url.URL) (*url.
} }
if len(b) == 0 { if len(b) == 0 {
return nil, errors.New("callNodeInfoWellKnown: response bytes was len 0") return nil, errors.New("dereferenceByNodeInfo: response bytes was len 0")
} }
wellKnownResp := &apimodel.WellKnownResponse{} wellKnownResp := &apimodel.WellKnownResponse{}
if err := json.Unmarshal(b, wellKnownResp); err != nil { if err := json.Unmarshal(b, wellKnownResp); err != nil {
return nil, fmt.Errorf("callNodeInfoWellKnown: could not unmarshal server response as WellKnownResponse: %s", err) return nil, fmt.Errorf("dereferenceByNodeInfo: could not unmarshal server response as WellKnownResponse: %s", err)
} }
// look through the links for the first one that matches the nodeinfo schema, this is what we need // look through the links for the first one that matches the nodeinfo schema, this is what we need
var nodeinfoHref *url.URL var nodeinfoHref *url.URL
for _, l := range wellKnownResp.Links { for _, l := range wellKnownResp.Links {
if l.Href == "" || !strings.HasPrefix(l.Rel, "http://nodeinfo.diaspora.software/ns/schema/2") { if l.Href == "" || !strings.HasPrefix(l.Rel, "http://nodeinfo.diaspora.software/ns/schema") {
continue continue
} }
nodeinfoHref, err = url.Parse(l.Href) nodeinfoHref, err = url.Parse(l.Href)
if err != nil { if err != nil {
return nil, fmt.Errorf("callNodeInfoWellKnown: couldn't parse url %s: %s", l.Href, err) return nil, fmt.Errorf("dereferenceByNodeInfo: couldn't parse url %s: %s", l.Href, err)
} }
} }
if nodeinfoHref == nil { if nodeinfoHref == nil {
return nil, errors.New("callNodeInfoWellKnown: could not find nodeinfo rel in well known response") return nil, errors.New("could not find nodeinfo rel in well known response")
} }
aaaaaaaaaaaaaaaaa // do the second query
return nodeinfoHref, nil return nil, errors.New("not yet implemented")
}
func callNodeInfo(c context.Context, t *transport, iri *url.URL) (*apimodel.Nodeinfo, error) {
l := t.log.WithField("func", "callNodeInfo")
l.Debugf("performing GET to %s", iri.String())
req, err := http.NewRequest("GET", iri.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(c)
req.Header.Add("Accept", "application/json")
req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
req.Header.Set("Host", iri.Host)
t.getSignerMu.Lock()
err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
t.getSignerMu.Unlock()
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("callNodeInfo: GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if len(b) == 0 {
return nil, errors.New("callNodeInfo: response bytes was len 0")
}
niResp := &apimodel.Nodeinfo{}
if err := json.Unmarshal(b, niResp); err != nil {
return nil, fmt.Errorf("callNodeInfo: could not unmarshal server response as Nodeinfo: %s", err)
}
return niResp, nil
} }