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

Remove API interface #27

Merged
merged 38 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4e7d906
Add Syncer (WIP commit)
peterjan Feb 10, 2022
14e8db8
Further implement the blocklist sync
peterjan Feb 15, 2022
a84288b
Add section in README.md
peterjan Feb 15, 2022
a7bf60f
Add missing nil check
peterjan Feb 15, 2022
f3b9836
Refactor the syncer and cover with unit tests
peterjan Feb 23, 2022
7f9be6d
Remove unused method
peterjan Feb 23, 2022
212a7fb
Add NOTE
peterjan Feb 23, 2022
b194545
Add testing for load env helpers
peterjan Feb 23, 2022
43d1901
Implement MR remarks
peterjan Feb 23, 2022
b39f6a6
Implement MR remarks
peterjan Feb 23, 2022
a5eb4c5
Add index on invalid
peterjan Feb 23, 2022
76b95d0
Implement MR remarks
peterjan Feb 24, 2022
4542200
Append unset err
peterjan Feb 24, 2022
807ab56
WIP commit
peterjan Feb 28, 2022
7edba61
Merge branch 'pj/blocklist-endpoint' of https://github.com/SkynetLabs…
peterjan Feb 28, 2022
46ae258
Implement MR remarks
peterjan Feb 28, 2022
1922d9f
Merge branch 'pj/blocklist-endpoint' of https://github.com/SkynetLabs…
peterjan Mar 1, 2022
c9b0fd9
Use portal blocklist and add tests
peterjan Mar 1, 2022
f6e2b41
Merge branch 'main' of https://github.com/SkynetLabs/blocker into pj/…
peterjan Mar 1, 2022
97cb484
Merge branch 'main' of https://github.com/SkynetLabs/blocker into pj/…
peterjan Mar 1, 2022
8589f7c
Cleanup PR
peterjan Mar 1, 2022
099e31b
Cleanup PR
peterjan Mar 1, 2022
3d18dfb
Check sync start error
peterjan Mar 1, 2022
724181b
Cleanup PR
peterjan Mar 1, 2022
6bf5052
Cleanup PR
peterjan Mar 3, 2022
e1b2f07
Cleanup PR
peterjan Mar 3, 2022
9bfe48f
Update start prop on blocker
peterjan Mar 4, 2022
abc6e84
Defer unlock
peterjan Mar 4, 2022
8c2d8b9
Implement MR remarks
peterjan Mar 4, 2022
34fbdaf
Implement MR remarks
peterjan Mar 7, 2022
4651cb1
Add testing
peterjan Mar 8, 2022
4b70799
Update endpoint
peterjan Mar 9, 2022
207a535
Add a unit test for the client
peterjan Mar 9, 2022
de2f2de
Remove the API interface
peterjan Mar 10, 2022
9a4a807
Merge branch 'main' of https://github.com/SkynetLabs/blocker into pj/…
peterjan Mar 10, 2022
f394f74
Implement MR remarks
peterjan Mar 16, 2022
3532489
Remove nginx client
peterjan Mar 16, 2022
b403311
Merge branch 'main' of https://github.com/SkynetLabs/blocker into pj/…
peterjan Mar 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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"
Expand All @@ -14,31 +13,31 @@ import (
// API is our central entry point to all subsystems relevant to serving
// requests.
type API struct {
staticDB *database.DB
staticLogger *logrus.Logger
staticRouter *httprouter.Router
staticSkydAPI skyd.API
staticDB *database.DB
staticLogger *logrus.Logger
staticRouter *httprouter.Router
staticSkydClient *Client
}

// New creates a new API instance.
func New(skydAPI skyd.API, db *database.DB, logger *logrus.Logger) (*API, error) {
func New(skydClient *Client, 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")
if skydClient == nil {
return nil, errors.New("no skyd client provided")
}
router := httprouter.New()
router.RedirectTrailingSlash = true

api := &API{
staticDB: db,
staticLogger: logger,
staticRouter: router,
staticSkydAPI: skydAPI,
staticDB: db,
staticLogger: logger,
staticRouter: router,
staticSkydClient: skydClient,
}

api.buildHTTPRoutes()
Expand Down
24 changes: 3 additions & 21 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import (
url "net/url"

"github.com/SkynetLabs/blocker/database"
"github.com/SkynetLabs/blocker/skyd"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/mongo/options"
)

// apiTester is a helper struct wrapping handlers of the underlying API that
Expand All @@ -27,32 +25,16 @@ func newAPITester(api *API) *apiTester {
}

// newTestAPI returns a new API instance
func newTestAPI(dbName string, skyd skyd.API) (*API, error) {
func newTestAPI(dbName string, client *Client) (*API, error) {
// create a nil logger
logger := logrus.New()
logger.Out = ioutil.Discard

// create database
db, err := database.NewCustomDB(context.Background(), "mongodb://localhost:37017", dbName, options.Credential{
Username: "admin",
Password: "aO4tV5tC1oU3oQ7u",
}, logger)
if err != nil {
return nil, err
}

// create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), database.MongoDefaultTimeout)
defer cancel()

// purge the database
err = db.Purge(ctx)
if err != nil {
panic(err)
}
db := database.NewTestDB(context.Background(), dbName, logger)

// create the API
api, err := New(skyd, db, logger)
api, err := New(client, db, logger)
if err != nil {
return nil, err
}
Expand Down
208 changes: 200 additions & 8 deletions api/client.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,101 @@
package api

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
url "net/url"
"net/url"

skyapi "gitlab.com/SkynetLabs/skyd/node/api"
"gitlab.com/SkynetLabs/skyd/skymodules"

"github.com/SkynetLabs/blocker/database"
"gitlab.com/NebulousLabs/errors"
"gitlab.com/SkynetLabs/skyd/node/api"
)

// Client is a helper struct that gets initialised using a portal url. It
// exposes API methods and abstracts the response handling.
type Client struct {
staticPortalURL string
}
const (
// clientDefaultTimeout is the timeout of the http calls to in seconds
clientDefaultTimeout = "30"
)

type (
// Client is a helper struct that gets initialised using a portal url. It
// exposes API methods and abstracts the response handling.
Client struct {
staticDefaultHeaders http.Header
staticPortalURL string
}

// BlockResponse is the response object returned by the Skyd API's block
// endpoint
BlockResponse struct {
Invalids []InvalidInput `json:"invalids"`
}

// DaemonReadyResponse is the response object returned by the Skyd API's
// ready endpoint
DaemonReadyResponse struct {
Ready bool `json:"ready"`
Consensus bool `json:"consensus"`
Gateway bool `json:"gateway"`
Renter bool `json:"renter"`
}

// InvalidInput is a struct that wraps the invalid input along with an error
// string indicating why it was deemed invalid
InvalidInput struct {
Input string `json:"input"`
Error string `json:"error"`
}

// resolveResponse is the response object returned by the Skyd API's resolve
// endpoint
resolveResponse struct {
Skylink string `json:"skylink"`
}
)

// NewClient returns a new Client instance for given portal url.
func NewClient(portalURL string) *Client {
return &Client{staticPortalURL: portalURL}
return NewCustomClient(portalURL, http.Header{})
}

// NewSkydClient returns a client that has the default user-agent set.
func NewSkydClient(portalURL string, headers http.Header) *Client {
headers.Add("User-Agent", "Sia-Agent")
return NewCustomClient(portalURL, headers)
}

// NewCustomClient returns a new Client instance for given portal url and lets
// you pass a set of headers that will be set on every request.
func NewCustomClient(portalURL string, headers http.Header) *Client {
return &Client{
staticDefaultHeaders: headers,
staticPortalURL: portalURL,
}
}

// InvalidHashes is a helper method that converts the list of invalid inputs to
// an array of hashes.
func (br *BlockResponse) InvalidHashes() ([]database.Hash, error) {
if len(br.Invalids) == 0 {
return nil, nil
}

hashes := make([]database.Hash, len(br.Invalids))
for i, invalid := range br.Invalids {
var h database.Hash
err := h.LoadString(invalid.Input)
if err != nil {
return nil, err
}
hashes[i] = h
}
return hashes, nil
}

// BlocklistGET calls the `/portal/blocklist` endpoint with given parameters
Expand All @@ -40,12 +115,96 @@ func (c *Client) BlocklistGET(offset int) (*BlocklistGET, error) {
return &blg, nil
}

// BlockHashes will perform an API call to skyd to block the given hashes. It
// returns which hashes were blocked, which hashes were invalid and potentially
// an error.
func (c *Client) BlockHashes(hashes []database.Hash) ([]database.Hash, []database.Hash, error) {
// convert the hashes to strings
adds := make([]string, len(hashes))
for h, hash := range hashes {
adds[h] = hash.String()
}

// build the post body
reqBody, err := json.Marshal(skyapi.SkynetBlocklistPOST{
Add: adds,
Remove: nil,
IsHash: true,
})
if err != nil {
return nil, nil, errors.AddContext(err, "failed to build request body")
}
body := bytes.NewBuffer(reqBody)

// build the query parameters
query := url.Values{}
query.Add("timeout", clientDefaultTimeout)

// execute the request
var response BlockResponse
err = c.post("/skynet/blocklist", query, body, &response)
if err != nil {
return nil, nil, errors.AddContext(err, "failed to execute POST request")
}

// parse the invalid hashes from the response
invalids, err := response.InvalidHashes()
if err != nil {
return nil, nil, errors.AddContext(err, "failed to parse invalid hashes from skyd response")
}

return database.DiffHashes(hashes, invalids), invalids, nil
}

// ResolveSkylink will resolve the given skylink.
func (c *Client) ResolveSkylink(skylink skymodules.Skylink) (skymodules.Skylink, error) {
// no need to resolve the skylink if it's a v1 skylink
if skylink.IsSkylinkV1() {
return skylink, nil
}

// execute the request
var response resolveResponse
endpoint := fmt.Sprintf("/skynet/resolve/%s", skylink.String())
err := c.get(endpoint, url.Values{}, &response)
if err != nil {
return skymodules.Skylink{}, errors.AddContext(err, "failed to execute GET request")
}

// check whether we resolved a valid skylink
err = skylink.LoadString(response.Skylink)
if err != nil {
return skymodules.Skylink{}, errors.AddContext(err, "unable to load the resolved skylink")
}
return skylink, nil
}

// DaemonReady connects to the local skyd and checks its status.
// Returns true only if skyd is fully ready.
func (c *Client) DaemonReady() bool {
var response DaemonReadyResponse
err := c.get("/daemon/ready", url.Values{}, &response)
if err != nil {
return false
}

return response.Ready &&
response.Consensus &&
response.Gateway &&
response.Renter
}

// get is a helper function that executes a GET request on the given endpoint
// with the provided query values. The response will get unmarshaled into the
// given response object.
func (c *Client) get(endpoint string, query url.Values, obj interface{}) error {
// create the request
url := fmt.Sprintf("%s%s?%s", c.staticPortalURL, endpoint, query.Encode())
queryString := query.Encode()
url := fmt.Sprintf("%s%s", c.staticPortalURL, endpoint)
if queryString != "" {
url = fmt.Sprintf("%s%s?%s", c.staticPortalURL, endpoint, queryString)
}

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return errors.AddContext(err, "failed to create request")
Expand All @@ -72,6 +231,39 @@ func (c *Client) get(endpoint string, query url.Values, obj interface{}) error {
return nil
}

// post is a helper function that executes a POST request on the given endpoint
// with the provided query values.
func (c *Client) post(endpoint string, query url.Values, body io.Reader, obj interface{}) error {
// create the request
url := fmt.Sprintf("%s%s?%s", c.staticPortalURL, endpoint, query.Encode())
req, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return errors.AddContext(err, "failed to create request")
}

// set headers and execute the request
for k, v := range c.staticDefaultHeaders {
req.Header.Set(k, v[0])
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer drainAndClose(res.Body)

// return an error if the status code is not in the 200s
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("GET request to '%s' with status %d error %v", url, res.StatusCode, readAPIError(res.Body))
}

// handle the response body
err = json.NewDecoder(res.Body).Decode(obj)
if err != nil {
return err
}
return nil
}

// drainAndClose reads rc until EOF and then closes it. drainAndClose should
// always be called on HTTP response bodies, because if the body is not fully
// read, the underlying connection can't be reused.
Expand Down
Loading