Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow List #11

Merged
merged 18 commits into from
Dec 21, 2021
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 13 additions & 7 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,39 @@ 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"
)

// 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()
Expand Down
28 changes: 23 additions & 5 deletions api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (
"gitlab.com/SkynetLabs/skyd/skymodules"
)

var (
// ErrSkylinkAllowListed is returned when we try to add a skylink to the
// database that is part of the allow list.
ErrSkylinkAllowListed = errors.New("skylink can not be blocked, it is allow listed")
)

type (
// BlockPOST describes a request to the /block endpoint.
BlockPOST struct {
Expand Down Expand Up @@ -93,11 +99,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
Expand All @@ -122,6 +128,12 @@ 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.WriteError(w, skyapi.Error{ErrSkylinkAllowListed.Error()}, http.StatusBadRequest)
return
}

// Block the link.
err = api.block(r.Context(), body.BlockPOST, sub, true)
if err != nil {
Expand Down Expand Up @@ -155,6 +167,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.WriteError(w, skyapi.Error{ErrSkylinkAllowListed.Error()}, http.StatusBadRequest)
return
}

// Block the link.
err = api.block(r.Context(), body, sub, sub == "")
if errors.Contains(err, database.ErrSkylinkExists) {
Expand Down
113 changes: 15 additions & 98 deletions blocker/blocker.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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(
Expand All @@ -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")
}
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)))
}
Loading