From 4d4326938a7f9f5e180493a61cf1fbdd2711a199 Mon Sep 17 00:00:00 2001 From: Nikita Kryuchkov Date: Sun, 5 May 2019 08:23:17 +0300 Subject: [PATCH 1/4] Implement `uptime-tracker` HTTP client --- internal/uptime-tracker/client/client.go | 89 +++++++++++++++++++ internal/uptime-tracker/client/client_test.go | 75 ++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 internal/uptime-tracker/client/client.go create mode 100644 internal/uptime-tracker/client/client_test.go diff --git a/internal/uptime-tracker/client/client.go b/internal/uptime-tracker/client/client.go new file mode 100644 index 0000000000..697a59d50b --- /dev/null +++ b/internal/uptime-tracker/client/client.go @@ -0,0 +1,89 @@ +// Package client implements uptime tracker client +package client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/skycoin/skywire/internal/httpauth" + "github.com/skycoin/skywire/pkg/cipher" +) + +// Error is the object returned to the client when there's an error. +type Error struct { + Error string `json:"error"` +} + +// APIClient implements messaging discovery API client. +type APIClient interface { + UpdateNodeUptime(context.Context) error +} + +// httpClient implements Client for uptime tracker API. +type httpClient struct { + client *httpauth.Client + key cipher.PubKey + sec cipher.SecKey +} + +// NewHTTP creates a new client setting a public key to the client to be used for auth. +// When keys are set, the client will sign request before submitting. +// The signature information is transmitted in the header using: +// * SW-Public: The specified public key +// * SW-Nonce: The nonce for that public key +// * SW-Sig: The signature of the payload + the nonce +func NewHTTP(addr string, key cipher.PubKey, sec cipher.SecKey) (APIClient, error) { + client, err := httpauth.NewClient(context.Background(), addr, key, sec) + if err != nil { + return nil, fmt.Errorf("httpauth: %s", err) + } + + return &httpClient{client: client, key: key, sec: sec}, nil +} + +// Get performs a new GET request. +func (c *httpClient) Get(ctx context.Context, path string) (*http.Response, error) { + req, err := http.NewRequest("GET", c.client.Addr+path, new(bytes.Buffer)) + if err != nil { + return nil, err + } + + return c.client.Do(req.WithContext(ctx)) +} + +// UpdateNodeUptime updates node uptime. +func (c *httpClient) UpdateNodeUptime(ctx context.Context) error { + resp, err := c.Get(ctx, "/update") + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status: %d, error: %v", resp.StatusCode, extractError(resp.Body)) + } + + return nil +} + +// extractError returns the decoded error message from Body. +func extractError(r io.Reader) error { + var apiError Error + + body, err := ioutil.ReadAll(r) + if err != nil { + return err + } + + if err := json.Unmarshal(body, &apiError); err != nil { + return errors.New(string(body)) + } + + return errors.New(apiError.Error) +} diff --git a/internal/uptime-tracker/client/client_test.go b/internal/uptime-tracker/client/client_test.go new file mode 100644 index 0000000000..b3efd60758 --- /dev/null +++ b/internal/uptime-tracker/client/client_test.go @@ -0,0 +1,75 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/skycoin/skywire/internal/httpauth" + "github.com/skycoin/skywire/pkg/cipher" +) + +var testPubKey, testSecKey = cipher.GenerateKeyPair() + +func TestClientAuth(t *testing.T) { + wg := sync.WaitGroup{} + + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + switch url := r.URL.String(); url { + case "/": + defer wg.Done() + assert.Equal(t, testPubKey.Hex(), r.Header.Get("SW-Public")) + assert.Equal(t, "1", r.Header.Get("SW-Nonce")) + assert.NotEmpty(t, r.Header.Get("SW-Sig")) // TODO: check for the right key + + case fmt.Sprintf("/security/nonces/%s", testPubKey): + fmt.Fprintf(w, `{"edge": "%s", "next_nonce": 1}`, testPubKey) + + default: + t.Errorf("Don't know how to handle URL = '%s'", url) + } + }, + )) + defer srv.Close() + + client, err := NewHTTP(srv.URL, testPubKey, testSecKey) + require.NoError(t, err) + c := client.(*httpClient) + + wg.Add(1) + _, err = c.Get(context.Background(), "/") + require.NoError(t, err) + + wg.Wait() +} + +func TestUpdateNodeUptime(t *testing.T) { + srv := httptest.NewServer(authHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/update", r.URL.String()) + }))) + defer srv.Close() + + c, err := NewHTTP(srv.URL, testPubKey, testSecKey) + require.NoError(t, err) + err = c.UpdateNodeUptime(context.Background()) + require.NoError(t, err) +} + +func authHandler(next http.Handler) http.Handler { + m := http.NewServeMux() + m.Handle("/security/nonces/", http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(&httpauth.NextNonceResponse{Edge: testPubKey, NextNonce: 1}) // nolint: errcheck + }, + )) + m.Handle("/", next) + return m +} From f9ec6753c086bc999681f240c71319b99fd68af2 Mon Sep 17 00:00:00 2001 From: Nikita Kryuchkov Date: Sun, 5 May 2019 08:37:26 +0300 Subject: [PATCH 2/4] Add `uptime-tracker` to config # Conflicts: # cmd/skywire-cli/commands/node/gen-config.go # pkg/node/config.go --- cmd/skywire-cli/commands/node/gen-config.go | 2 ++ pkg/visor/config.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/cmd/skywire-cli/commands/node/gen-config.go b/cmd/skywire-cli/commands/node/gen-config.go index ab132ea19a..1058b00f91 100644 --- a/cmd/skywire-cli/commands/node/gen-config.go +++ b/cmd/skywire-cli/commands/node/gen-config.go @@ -111,6 +111,8 @@ func defaultConfig() *visor.Config { conf.Hypervisors = []visor.HypervisorConfig{} + conf.Uptime.Tracker = "" + conf.AppsPath = "./apps" conf.LocalPath = "./local" diff --git a/pkg/visor/config.go b/pkg/visor/config.go index 8b603f2804..fdbfef03f8 100644 --- a/pkg/visor/config.go +++ b/pkg/visor/config.go @@ -48,6 +48,10 @@ type Config struct { } `json:"table"` } `json:"routing"` + Uptime struct { + Tracker string `json:"tracker"` + } `json:"uptime"` + Apps []AppConfig `json:"apps"` TrustedNodes []cipher.PubKey `json:"trusted_nodes"` From 47fd7bc992c3883da7580365548c76df9ee6020f Mon Sep 17 00:00:00 2001 From: Nikita Kryuchkov Date: Wed, 21 Aug 2019 15:01:28 +0400 Subject: [PATCH 3/4] Implement sending data to `uptime-tracker` in `skywire-node` # Conflicts: # cmd/skywire-node/commands/root.go --- cmd/skywire-visor/commands/root.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cmd/skywire-visor/commands/root.go b/cmd/skywire-visor/commands/root.go index e07395451b..9e71578e92 100644 --- a/cmd/skywire-visor/commands/root.go +++ b/cmd/skywire-visor/commands/root.go @@ -2,6 +2,7 @@ package commands import ( "bufio" + "context" "encoding/json" "fmt" "io" @@ -22,6 +23,7 @@ import ( "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" + utClient "github.com/skycoin/skywire/internal/uptime-tracker/client" "github.com/skycoin/skywire/pkg/util/pathutil" "github.com/skycoin/skywire/pkg/visor" ) @@ -148,6 +150,28 @@ func (cfg *runCfg) runNode() *runCfg { cfg.logger.Fatal("Failed to initialize node: ", err) } + go func() { + if cfg.conf.Uptime.Tracker == "" { + return + } + + uptimeTracker, err := utClient.NewHTTP(cfg.conf.Uptime.Tracker, cfg.conf.Node.StaticPubKey, cfg.conf.Node.StaticSecKey) + if err != nil { + cfg.logger.Error("Failed to connect to uptime tracker: ", err) + return + } + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for range ticker.C { + ctx := context.Background() + if err := uptimeTracker.UpdateNodeUptime(ctx); err != nil { + cfg.logger.Error("Failed to update node uptime: ", err) + } + } + }() + go func() { if err := node.Start(); err != nil { cfg.logger.Fatal("Failed to start node: ", err) From 6e250c3295e7fdd51e108765a2fb76e9389ed854 Mon Sep 17 00:00:00 2001 From: Nikita Kryuchkov Date: Mon, 6 May 2019 14:54:28 +0300 Subject: [PATCH 4/4] Code cleanup # Conflicts: # cmd/skywire-node/commands/root.go --- cmd/skywire-visor/commands/root.go | 35 +++++++++---------- .../client => utclient}/client.go | 19 +++++----- .../client => utclient}/client_test.go | 24 ++++++++----- 3 files changed, 42 insertions(+), 36 deletions(-) rename internal/{uptime-tracker/client => utclient}/client.go (80%) rename internal/{uptime-tracker/client => utclient}/client_test.go (75%) diff --git a/cmd/skywire-visor/commands/root.go b/cmd/skywire-visor/commands/root.go index 9e71578e92..a2d738559a 100644 --- a/cmd/skywire-visor/commands/root.go +++ b/cmd/skywire-visor/commands/root.go @@ -23,7 +23,7 @@ import ( "github.com/skycoin/skycoin/src/util/logging" "github.com/spf13/cobra" - utClient "github.com/skycoin/skywire/internal/uptime-tracker/client" + "github.com/skycoin/skywire/internal/utclient" "github.com/skycoin/skywire/pkg/util/pathutil" "github.com/skycoin/skywire/pkg/visor" ) @@ -150,27 +150,24 @@ func (cfg *runCfg) runNode() *runCfg { cfg.logger.Fatal("Failed to initialize node: ", err) } - go func() { - if cfg.conf.Uptime.Tracker == "" { - return - } - - uptimeTracker, err := utClient.NewHTTP(cfg.conf.Uptime.Tracker, cfg.conf.Node.StaticPubKey, cfg.conf.Node.StaticSecKey) + if cfg.conf.Uptime.Tracker != "" { + uptimeTracker, err := utclient.NewHTTP(cfg.conf.Uptime.Tracker, cfg.conf.Node.StaticPubKey, cfg.conf.Node.StaticSecKey) if err != nil { cfg.logger.Error("Failed to connect to uptime tracker: ", err) - return - } - - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for range ticker.C { - ctx := context.Background() - if err := uptimeTracker.UpdateNodeUptime(ctx); err != nil { - cfg.logger.Error("Failed to update node uptime: ", err) - } + } else { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + go func() { + for range ticker.C { + ctx := context.Background() + if err := uptimeTracker.UpdateNodeUptime(ctx); err != nil { + cfg.logger.Error("Failed to update node uptime: ", err) + } + } + }() } - }() + } go func() { if err := node.Start(); err != nil { diff --git a/internal/uptime-tracker/client/client.go b/internal/utclient/client.go similarity index 80% rename from internal/uptime-tracker/client/client.go rename to internal/utclient/client.go index 697a59d50b..1cb27348fb 100644 --- a/internal/uptime-tracker/client/client.go +++ b/internal/utclient/client.go @@ -1,5 +1,5 @@ -// Package client implements uptime tracker client -package client +// Package utclient implements uptime tracker client +package utclient import ( "bytes" @@ -11,8 +11,9 @@ import ( "io/ioutil" "net/http" + "github.com/skycoin/dmsg/cipher" + "github.com/skycoin/skywire/internal/httpauth" - "github.com/skycoin/skywire/pkg/cipher" ) // Error is the object returned to the client when there's an error. @@ -28,8 +29,8 @@ type APIClient interface { // httpClient implements Client for uptime tracker API. type httpClient struct { client *httpauth.Client - key cipher.PubKey - sec cipher.SecKey + pk cipher.PubKey + sk cipher.SecKey } // NewHTTP creates a new client setting a public key to the client to be used for auth. @@ -38,18 +39,18 @@ type httpClient struct { // * SW-Public: The specified public key // * SW-Nonce: The nonce for that public key // * SW-Sig: The signature of the payload + the nonce -func NewHTTP(addr string, key cipher.PubKey, sec cipher.SecKey) (APIClient, error) { - client, err := httpauth.NewClient(context.Background(), addr, key, sec) +func NewHTTP(addr string, pk cipher.PubKey, sk cipher.SecKey) (APIClient, error) { + client, err := httpauth.NewClient(context.Background(), addr, pk, sk) if err != nil { return nil, fmt.Errorf("httpauth: %s", err) } - return &httpClient{client: client, key: key, sec: sec}, nil + return &httpClient{client: client, pk: pk, sk: sk}, nil } // Get performs a new GET request. func (c *httpClient) Get(ctx context.Context, path string) (*http.Response, error) { - req, err := http.NewRequest("GET", c.client.Addr+path, new(bytes.Buffer)) + req, err := http.NewRequest("GET", c.client.Addr()+path, new(bytes.Buffer)) if err != nil { return nil, err } diff --git a/internal/uptime-tracker/client/client_test.go b/internal/utclient/client_test.go similarity index 75% rename from internal/uptime-tracker/client/client_test.go rename to internal/utclient/client_test.go index b3efd60758..f3e16e3370 100644 --- a/internal/uptime-tracker/client/client_test.go +++ b/internal/utclient/client_test.go @@ -1,4 +1,4 @@ -package client +package utclient import ( "context" @@ -9,11 +9,11 @@ import ( "sync" "testing" + "github.com/skycoin/dmsg/cipher" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/skycoin/skywire/internal/httpauth" - "github.com/skycoin/skywire/pkg/cipher" ) var testPubKey, testSecKey = cipher.GenerateKeyPair() @@ -21,14 +21,13 @@ var testPubKey, testSecKey = cipher.GenerateKeyPair() func TestClientAuth(t *testing.T) { wg := sync.WaitGroup{} + headerCh := make(chan http.Header, 1) srv := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { switch url := r.URL.String(); url { case "/": defer wg.Done() - assert.Equal(t, testPubKey.Hex(), r.Header.Get("SW-Public")) - assert.Equal(t, "1", r.Header.Get("SW-Nonce")) - assert.NotEmpty(t, r.Header.Get("SW-Sig")) // TODO: check for the right key + headerCh <- r.Header case fmt.Sprintf("/security/nonces/%s", testPubKey): fmt.Fprintf(w, `{"edge": "%s", "next_nonce": 1}`, testPubKey) @@ -45,22 +44,31 @@ func TestClientAuth(t *testing.T) { c := client.(*httpClient) wg.Add(1) - _, err = c.Get(context.Background(), "/") + _, err = c.Get(context.TODO(), "/") require.NoError(t, err) + header := <-headerCh + assert.Equal(t, testPubKey.Hex(), header.Get("SW-Public")) + assert.Equal(t, "1", header.Get("SW-Nonce")) + assert.NotEmpty(t, header.Get("SW-Sig")) // TODO: check for the right key + wg.Wait() } func TestUpdateNodeUptime(t *testing.T) { + urlCh := make(chan string, 1) srv := httptest.NewServer(authHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/update", r.URL.String()) + urlCh <- r.URL.String() }))) defer srv.Close() c, err := NewHTTP(srv.URL, testPubKey, testSecKey) require.NoError(t, err) - err = c.UpdateNodeUptime(context.Background()) + + err = c.UpdateNodeUptime(context.TODO()) require.NoError(t, err) + + assert.Equal(t, "/update", <-urlCh) } func authHandler(next http.Handler) http.Handler {