diff --git a/README.md b/README.md index 0b2f297..f02d58e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,23 @@ The service exposes a REST API that allows callers to request the blocking of ne The blocklist is shared between the servers that make up a portal cluster via MongoDB. +# AllowList + +The blocker service can only block skylinks which are not in the allow list. +To add a skylink to the allow list, one has to manually query the database and +perform the follow operation: + +``` +db.getCollection('allowlist').insertOne({ + skylink: "[INSERT V1 SKYLINK HERE]", + description: "[INSERT SKYLINK DESCRIPTION]", + timestamp_added: new Date(), +}) +``` + +The skylink is expected to be in the following form: `_B19BtlWtjjR7AD0DDzxYanvIhZ7cxXrva5tNNxDht1kaA`. +So that's without portal and without the `sia://` prefix. + # Environment This service depends on the following environment variables: diff --git a/api/api.go b/api/api.go index cb0123d..d1b66c1 100644 --- a/api/api.go +++ b/api/api.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/SkynetLabs/blocker/database" + "github.com/SkynetLabs/blocker/skyd" "github.com/julienschmidt/httprouter" "github.com/sirupsen/logrus" "gitlab.com/NebulousLabs/errors" @@ -12,26 +13,31 @@ import ( // API is our central entry point to all subsystems relevant to serving requests. type API struct { - staticDB *database.DB - staticRouter *httprouter.Router - staticLogger *logrus.Logger + staticDB *database.DB + staticLogger *logrus.Logger + staticRouter *httprouter.Router + staticSkydAPI *skyd.SkydAPI } // New creates a new API instance. -func New(db *database.DB, logger *logrus.Logger) (*API, error) { +func New(skydAPI *skyd.SkydAPI, db *database.DB, logger *logrus.Logger) (*API, error) { if db == nil { return nil, errors.New("no DB provided") } if logger == nil { return nil, errors.New("no logger provided") } + if skydAPI == nil { + return nil, errors.New("no skyd API provided") + } router := httprouter.New() router.RedirectTrailingSlash = true api := &API{ - staticDB: db, - staticRouter: router, - staticLogger: logger, + staticDB: db, + staticLogger: logger, + staticRouter: router, + staticSkydAPI: skydAPI, } api.buildHTTPRoutes() diff --git a/api/handlers.go b/api/handlers.go index 9b50d66..53df43c 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -93,11 +93,11 @@ func (api *API) healthGET(w http.ResponseWriter, r *http.Request, _ httprouter.P skyapi.WriteJSON(w, status) } -// blockWithPoWPOST blocks a skylink. It is meant to be used by untrusted sources such as -// the abuse report skapp. The PoW prevents users from easily and anonymously -// blocking large numbers of skylinks. Instead it encourages reuse of proofs -// which improves the linkability between reports, thus allowing us to more -// easily unblock a batch of links. +// blockWithPoWPOST blocks a skylink. It is meant to be used by untrusted +// sources such as the abuse report skapp. The PoW prevents users from easily +// and anonymously blocking large numbers of skylinks. Instead it encourages +// reuse of proofs which improves the linkability between reports, thus allowing +// us to more easily unblock a batch of links. func (api *API) blockWithPoWPOST(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { // Protect against large bodies. b := http.MaxBytesReader(w, r.Body, 1<<16) // 64 kib @@ -122,12 +122,18 @@ func (api *API) blockWithPoWPOST(w http.ResponseWriter, r *http.Request, _ httpr // reporter authenticated. sub := hex.EncodeToString(body.PoW.MySkyID[:]) + // Check whether the skylink is on the allow list + if api.staticSkydAPI.IsAllowListed(r.Context(), string(body.Skylink)) { + skyapi.WriteJSON(w, statusResponse{"reported"}) + return + } + // Block the link. err = api.block(r.Context(), body.BlockPOST, sub, true) if err != nil { skyapi.WriteError(w, skyapi.Error{err.Error()}, http.StatusInternalServerError) } - skyapi.WriteSuccess(w) + skyapi.WriteJSON(w, statusResponse{"reported"}) } // blockWithPoWGET is the handler for the /blockpow [GET] endpoint. @@ -155,6 +161,12 @@ func (api *API) blockPOST(w http.ResponseWriter, r *http.Request, _ httprouter.P } } + // Check whether the skylink is on the allow list + if api.staticSkydAPI.IsAllowListed(r.Context(), string(body.Skylink)) { + skyapi.WriteJSON(w, statusResponse{"reported"}) + return + } + // Block the link. err = api.block(r.Context(), body, sub, sub == "") if errors.Contains(err, database.ErrSkylinkExists) { @@ -165,7 +177,7 @@ func (api *API) blockPOST(w http.ResponseWriter, r *http.Request, _ httprouter.P skyapi.WriteError(w, skyapi.Error{err.Error()}, http.StatusInternalServerError) return } - skyapi.WriteJSON(w, statusResponse{"blocked"}) + skyapi.WriteJSON(w, statusResponse{"reported"}) } // block blocks a skylink diff --git a/blocker/blocker.go b/blocker/blocker.go index abd9a17..51fbb1b 100644 --- a/blocker/blocker.go +++ b/blocker/blocker.go @@ -1,23 +1,17 @@ package blocker import ( - "bytes" "context" - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" "os" "sort" "strings" "time" "github.com/SkynetLabs/blocker/database" + "github.com/SkynetLabs/blocker/skyd" "github.com/SkynetLabs/skynet-accounts/build" "github.com/sirupsen/logrus" "gitlab.com/NebulousLabs/errors" - skyapi "gitlab.com/SkynetLabs/skyd/node/api" ) const ( @@ -27,11 +21,6 @@ const ( ) var ( - // ErrSkydOffline is returned if skyd is unreachable on startup. - ErrSkydOffline = errors.New("skyd is offline") - - // skydTimeout is the timeout of the http calls to skyd in seconds - skydTimeout = "30" // sleepBetweenScans defines how long the scanner should sleep after // scanning the DB and not finding any skylinks to scan. sleepBetweenScans = build.Select( @@ -57,20 +46,17 @@ var ( // Blocker scans the database for skylinks that should be blocked and calls // skyd to block them. type Blocker struct { - staticSkydHost string - staticSkydPort int - staticSkydAPIPassword string - staticNginxCachePurgerListPath string staticNginxCachePurgeLockPath string - staticCtx context.Context - staticDB *database.DB - staticLogger *logrus.Logger + staticCtx context.Context + staticDB *database.DB + staticLogger *logrus.Logger + staticSkydAPI *skyd.SkydAPI } // New returns a new Blocker with the given parameters. -func New(ctx context.Context, db *database.DB, logger *logrus.Logger, skydHost, skydPassword string, skydPort int, nginxCachePurgerListPath, nginxCachePurgeLockPath string) (*Blocker, error) { +func New(ctx context.Context, skydAPI *skyd.SkydAPI, db *database.DB, logger *logrus.Logger, nginxCachePurgerListPath, nginxCachePurgeLockPath string) (*Blocker, error) { if ctx == nil { return nil, errors.New("invalid context provided") } @@ -80,54 +66,21 @@ func New(ctx context.Context, db *database.DB, logger *logrus.Logger, skydHost, if logger == nil { return nil, errors.New("invalid logger provided") } + if skydAPI == nil { + return nil, errors.New("invalid Skyd API provided") + } bl := &Blocker{ - staticSkydHost: skydHost, - staticSkydPort: skydPort, - staticSkydAPIPassword: skydPassword, - staticNginxCachePurgerListPath: nginxCachePurgerListPath, staticNginxCachePurgeLockPath: nginxCachePurgeLockPath, - staticCtx: ctx, - staticDB: db, - staticLogger: logger, - } - if !bl.staticIsSkydUp() { - return nil, ErrSkydOffline + staticCtx: ctx, + staticDB: db, + staticLogger: logger, + staticSkydAPI: skydAPI, } return bl, nil } -// staticIsSkydUp connects to the local skyd and checks its status. -// Returns true only if skyd is fully ready. -func (bl *Blocker) staticIsSkydUp() bool { - status := struct { - Ready bool - Consensus bool - Gateway bool - Renter bool - }{} - url := fmt.Sprintf("http://%s:%d/daemon/ready", bl.staticSkydHost, bl.staticSkydPort) - r, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - bl.staticLogger.Error(err) - return false - } - r.Header.Set("User-Agent", "Sia-Agent") - resp, err := http.DefaultClient.Do(r) - if err != nil { - bl.staticLogger.Warnf("Failed to query skyd: %s", err.Error()) - return false - } - defer resp.Body.Close() - err = json.NewDecoder(resp.Body).Decode(&status) - if err != nil { - bl.staticLogger.Warnf("Bad body from skyd's /daemon/ready: %s", err.Error()) - return false - } - return status.Ready && status.Consensus && status.Gateway && status.Renter -} - // SweepAndBlock sweeps the DB for new skylinks, blocks them in skyd and writes // down the timestamp of the latest one, so it will scan from that moment // further on its next sweep. @@ -254,40 +207,10 @@ func (bl *Blocker) blockSkylinks(sls []string) error { if err != nil { bl.staticLogger.Warnf("Failed to write to nginx cache purger's list: %s", err) } - // Build the call to skyd. - reqBody := skyapi.SkynetBlocklistPOST{ - Add: sls, - Remove: nil, - IsHash: false, - } - reqBodyBytes, err := json.Marshal(reqBody) - if err != nil { - return errors.AddContext(err, "failed to build request body") - } - url := fmt.Sprintf("http://%s:%d/skynet/blocklist?timeout=%s", bl.staticSkydHost, bl.staticSkydPort, skydTimeout) - bl.staticLogger.Debugf("blockSkylinks: POST on %+s", url) - req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBodyBytes)) - if err != nil { - return errors.AddContext(err, "failed to build request to skyd") - } - req.Header.Set("User-Agent", "Sia-Agent") - req.Header.Set("Authorization", bl.staticAuthHeader()) - bl.staticLogger.Debugf("blockSkylinks: headers: %+v", req.Header) - resp, err := http.DefaultClient.Do(req) + err = bl.staticSkydAPI.BlockSkylinks(sls) if err != nil { - return errors.AddContext(err, "failed to make request to skyd") - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - bl.staticLogger.Warn(errors.AddContext(err, "failed to parse response body after a failed call to skyd").Error()) - respBody = []byte{} - } - err = errors.New(fmt.Sprintf("call to skyd failed with status '%s' and response '%s'", resp.Status, string(respBody))) - bl.staticLogger.Warn(err.Error()) - return err + return errors.AddContext(err, "block skylinks failed") } return nil } @@ -346,9 +269,3 @@ func (bl *Blocker) writeToNginxCachePurger(sls []string) error { } return nil } - -// staticAuthHeader returns the value we need to set to the `Authorization` -// header in order to call `skyd`. -func (bl *Blocker) staticAuthHeader() string { - return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(":"+bl.staticSkydAPIPassword))) -} diff --git a/database/database.go b/database/database.go index 82c05cd..94368fb 100644 --- a/database/database.go +++ b/database/database.go @@ -17,7 +17,7 @@ import ( var ( // ErrNoDocumentsFound is returned when a database operation completes // successfully but it doesn't find or affect any documents. - ErrNoDocumentsFound = errors.New("no documents found") + ErrNoDocumentsFound = errors.New("no documents") // ErrSkylinkExists is returned when we try to add a skylink to the database // and it already exists there. ErrSkylinkExists = errors.New("skylink already exists") @@ -34,6 +34,8 @@ var ( dbName = "blocker" // dbSkylinks defines the name of the skylinks collection dbSkylinks = "skylinks" + // dbAllowList defines the name of the allowlist collection + dbAllowList = "allowlist" // dbLatestBlockTimestamps dbLatestBlockTimestamps dbLatestBlockTimestamps = "latest_block_timestamps" ) @@ -41,11 +43,12 @@ var ( // DB holds a connection to the database, as well as helpful shortcuts to // collections and utilities. type DB struct { - ctx context.Context - staticClient *mongo.Client - staticDB *mongo.Database - staticSkylinks *mongo.Collection - staticLogger *logrus.Logger + ctx context.Context + staticClient *mongo.Client + staticDB *mongo.Database + staticAllowList *mongo.Collection + staticSkylinks *mongo.Collection + staticLogger *logrus.Logger } // New creates a new database connection. @@ -87,11 +90,12 @@ func NewCustomDB(ctx context.Context, uri string, dbName string, creds options.C return nil, err } return &DB{ - ctx: ctx, - staticClient: c, - staticDB: db, - staticSkylinks: db.Collection(dbSkylinks), - staticLogger: logger, + ctx: ctx, + staticClient: c, + staticDB: db, + staticAllowList: db.Collection(dbAllowList), + staticSkylinks: db.Collection(dbSkylinks), + staticLogger: logger, }, nil } @@ -138,6 +142,18 @@ func (db *DB) CreateBlockedSkylink(ctx context.Context, skylink *BlockedSkylink) return err } +// IsAllowListed returns whether the given skylink is on the allow list. +func (db *DB) IsAllowListed(ctx context.Context, skylink string) (bool, error) { + res := db.staticAllowList.FindOne(ctx, bson.M{"skylink": skylink}) + if isDocumentNotFound(res.Err()) { + return false, nil + } + if res.Err() != nil { + return false, res.Err() + } + return true, nil +} + // BlockedSkylinkSave saves the given BlockedSkylink record to the database. // NOTE: commented out since this method isn't used or tested. //func (db *DB) BlockedSkylinkSave(ctx context.Context, skylink *BlockedSkylink) error { @@ -226,6 +242,16 @@ func ensureDBSchema(ctx context.Context, db *mongo.Database, log *logrus.Logger) // schema defines a mapping between a collection name and the indexes that // must exist for that collection. schema := map[string][]mongo.IndexModel{ + dbAllowList: { + { + Keys: bson.D{{"skylink", 1}}, + Options: options.Index().SetName("skylink").SetUnique(true), + }, + { + Keys: bson.D{{"timestamp_added", 1}}, + Options: options.Index().SetName("timestamp_added"), + }, + }, dbSkylinks: { { Keys: bson.D{{"skylink", 1}}, @@ -276,3 +302,12 @@ func ensureCollection(ctx context.Context, db *mongo.Database, collName string) } return coll, nil } + +// isDocumentNotFound is a helper function that returns whether the given error +// contains the mongo documents not found error message. +func isDocumentNotFound(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), ErrNoDocumentsFound.Error()) +} diff --git a/database/database_test.go b/database/database_test.go index 71aeb83..51c2a73 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -48,6 +48,10 @@ func TestDatabase(t *testing.T) { name: "CreateBlockedSkylink", test: testCreateBlockedSkylink, }, + { + name: "IsAllowListedSkylink", + test: testIsAllowListedSkylink, + }, } for _, test := range tests { t.Run(test.name, test.test) @@ -119,3 +123,39 @@ func testCreateBlockedSkylink(t *testing.T) { t.Fatal("not equal") } } + +// testIsAllowListedSkylink tests the 'IsAllowListed' method on the database. +func testIsAllowListedSkylink(t *testing.T) { + db := newTestDB(t.Name()) + defer db.Close() + + // Add a skylink in the allow list + skylink := "_B19BtlWtjjR7AD0DDzxYanvIhZ7cxXrva5tNNxDht1kaA" + _, err := db.staticAllowList.InsertOne(context.Background(), &AllowListedSkylink{ + Skylink: skylink, + Description: "test skylink", + TimestampAdded: time.Now().UTC(), + }) + if err != nil { + t.Fatal(err) + } + + // Check the result of 'IsAllowListed' + allowListed, err := db.IsAllowListed(context.Background(), skylink) + if err != nil { + t.Fatal(err) + } + if !allowListed { + t.Fatal("unexpected") + } + + // Check against a different skylink + skylink = "ABC9BtlWtjjR7AD0DDzxYanvIhZ7cxXrva5tNNxDht1ABC" + allowListed, err = db.IsAllowListed(context.Background(), skylink) + if err != nil { + t.Fatal(err) + } + if allowListed { + t.Fatal("unexpected") + } +} diff --git a/database/skylink.go b/database/skylink.go index b41157b..a2de983 100644 --- a/database/skylink.go +++ b/database/skylink.go @@ -6,6 +6,15 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) +// AllowListedSkylink is a skylink that is allow listed and thus prohibited from +// ever being blocked. +type AllowListedSkylink struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Skylink string `bson:"skylink"` + Description string `bson:"description"` + TimestampAdded time.Time `bson:"timestamp_added"` +} + // BlockedSkylink is a skylink blocked by an external request. type BlockedSkylink struct { ID primitive.ObjectID `bson:"_id,omitempty"` diff --git a/main.go b/main.go index 1f5dd22..7c21938 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/SkynetLabs/blocker/api" "github.com/SkynetLabs/blocker/blocker" "github.com/SkynetLabs/blocker/database" + "github.com/SkynetLabs/blocker/skyd" "github.com/joho/godotenv" "github.com/sirupsen/logrus" "gitlab.com/NebulousLabs/errors" @@ -139,11 +140,17 @@ func main() { nginxCachePurgeLockPath = nginxLock } - // Create the blocker. - blockerThread, err := blocker.New(ctx, db, logger, skydHost, skydAPIPassword, skydPort, nginxCachePurgerListPath, nginxCachePurgeLockPath) - if errors.Contains(err, blocker.ErrSkydOffline) { + // Create a skyd API. + skydAPI, err := skyd.NewSkydAPI(skydHost, skydAPIPassword, skydPort, db, logger) + if err != nil { + log.Fatal(errors.AddContext(err, "failed to instantiate Skyd API")) + } + if !skydAPI.IsSkydUp() { log.Fatal(errors.New("skyd down, exiting")) } + + // Create the blocker. + blockerThread, err := blocker.New(ctx, skydAPI, db, logger, nginxCachePurgerListPath, nginxCachePurgeLockPath) if err != nil { log.Fatal(errors.AddContext(err, "failed to instantiate blocker")) } @@ -152,7 +159,7 @@ func main() { blockerThread.Start() // Initialise the server. - server, err := api.New(db, logger) + server, err := api.New(skydAPI, db, logger) if err != nil { log.Fatal(errors.AddContext(err, "failed to build the api")) } diff --git a/skyd/api.go b/skyd/api.go new file mode 100644 index 0000000..c1bb2c1 --- /dev/null +++ b/skyd/api.go @@ -0,0 +1,180 @@ +package skyd + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + skyapi "gitlab.com/SkynetLabs/skyd/node/api" + + "github.com/SkynetLabs/blocker/database" + "github.com/sirupsen/logrus" + "gitlab.com/NebulousLabs/errors" +) + +const ( + // skydTimeout is the timeout of the http calls to skyd in seconds + skydTimeout = "30" +) + +// SkydAPI is a helper struct that exposes some methods that allow making skyd +// API calls used by both the API and the blocker +type SkydAPI struct { + staticSkydHost string + staticSkydPort int + staticSkydAPIPassword string + + staticDB *database.DB + staticLogger *logrus.Logger +} + +// NewSkydAPI creates a new Skyd API instance. +func NewSkydAPI(skydHost, skydPassword string, skydPort int, db *database.DB, logger *logrus.Logger) (*SkydAPI, error) { + if db == nil { + return nil, errors.New("no DB provided") + } + if logger == nil { + return nil, errors.New("no logger provided") + } + + return &SkydAPI{ + staticSkydHost: skydHost, + staticSkydPort: skydPort, + staticSkydAPIPassword: skydPassword, + + staticDB: db, + staticLogger: logger, + }, nil +} + +// BlockSkylinks will perform an API call to skyd to block the given skylinks +func (skyd *SkydAPI) BlockSkylinks(sls []string) error { + // Build the call to skyd. + reqBody := skyapi.SkynetBlocklistPOST{ + Add: sls, + Remove: nil, + IsHash: false, + } + reqBodyBytes, err := json.Marshal(reqBody) + if err != nil { + return errors.AddContext(err, "failed to build request body") + } + + url := fmt.Sprintf("http://%s:%d/skynet/blocklist?timeout=%s", skyd.staticSkydHost, skyd.staticSkydPort, skydTimeout) + + skyd.staticLogger.Debugf("blockSkylinks: POST on %+s", url) + req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBodyBytes)) + if err != nil { + return errors.AddContext(err, "failed to build request to skyd") + } + req.Header.Set("User-Agent", "Sia-Agent") + req.Header.Set("Authorization", skyd.staticAuthHeader()) + + skyd.staticLogger.Debugf("blockSkylinks: headers: %+v", req.Header) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return errors.AddContext(err, "failed to make request to skyd") + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + skyd.staticLogger.Warn(errors.AddContext(err, "failed to parse response body after a failed call to skyd").Error()) + respBody = []byte{} + } + err = errors.New(fmt.Sprintf("call to skyd failed with status '%s' and response '%s'", resp.Status, string(respBody))) + skyd.staticLogger.Warn(err.Error()) + return err + } + return nil +} + +// IsAllowListed will resolve the given skylink and verify it against the +// allow list, it returns true if the skylink is present on the allow list +func (skyd *SkydAPI) IsAllowListed(ctx context.Context, skylink string) bool { + // build the request to resolve the skylink with skyd + url := fmt.Sprintf("http://%s:%d/skynet/resolve/%s", skyd.staticSkydHost, skyd.staticSkydPort, skylink) + skyd.staticLogger.Debugf("isAllowListed: GET on %+s", url) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + skyd.staticLogger.Error("failed to build request to skyd", err) + return false + } + + // set headers and execute the request + req.Header.Set("User-Agent", "Sia-Agent") + req.Header.Set("Authorization", skyd.staticAuthHeader()) + resp, err := http.DefaultClient.Do(req) + if err != nil { + skyd.staticLogger.Error("failed to make request to skyd", err) + return false + } + defer resp.Body.Close() + + // if the skylink was blocked it was not allow listed + if resp.StatusCode == http.StatusUnavailableForLegalReasons { + return false + } + + // if the status code is 200 OK, swap the skylink against the resolved + // skylink before checking it against the allow list + if resp.StatusCode == http.StatusOK { + resolved := struct { + Skylink string + }{} + err = json.NewDecoder(resp.Body).Decode(&resolved) + if err != nil { + skyd.staticLogger.Error("bad response body from skyd", err) + return false + } + skylink = resolved.Skylink + } + + // check whether the skylink is allow listed + allowlisted, err := skyd.staticDB.IsAllowListed(ctx, skylink) + if err != nil { + skyd.staticLogger.Error("failed to verify skylink against the allow list", err) + return false + } + return allowlisted +} + +// IsSkydUp connects to the local skyd and checks its status. +// Returns true only if skyd is fully ready. +func (skyd *SkydAPI) IsSkydUp() bool { + status := struct { + Ready bool + Consensus bool + Gateway bool + Renter bool + }{} + url := fmt.Sprintf("http://%s:%d/daemon/ready", skyd.staticSkydHost, skyd.staticSkydPort) + r, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + skyd.staticLogger.Error(err) + return false + } + r.Header.Set("User-Agent", "Sia-Agent") + resp, err := http.DefaultClient.Do(r) + if err != nil { + skyd.staticLogger.Warnf("Failed to query skyd: %s", err.Error()) + return false + } + defer resp.Body.Close() + err = json.NewDecoder(resp.Body).Decode(&status) + if err != nil { + skyd.staticLogger.Warnf("Bad body from skyd's /daemon/ready: %s", err.Error()) + return false + } + return status.Ready && status.Consensus && status.Gateway && status.Renter +} + +// staticAuthHeader returns the value we need to set to the `Authorization` +// header in order to call `skyd`. +func (skyd *SkydAPI) staticAuthHeader() string { + return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(":"+skyd.staticSkydAPIPassword))) +}