From 95845c88feaa109d6b3821aa02e5a7ade31df4c9 Mon Sep 17 00:00:00 2001 From: Mikhail Bragin Date: Mon, 23 Aug 2021 21:43:05 +0200 Subject: [PATCH] Extend peer http endpoint (#94) * feature: add peer GET and DELETE API methods * refactor: extract peer business logic to a separate file * refactor: extract peer business logic to a separate file * feature: add peer update HTTP endpoint * chore: fill peer new fields * merge with main * refactor: HTTP methods according to standards * chore: setup keys POST endpoint without ID --- management/server/account.go | 105 ----------- management/server/file_store.go | 45 +++++ management/server/http/handler/peers.go | 98 +++++++++-- management/server/http/handler/setupkeys.go | 101 ++++++----- management/server/http/handler/util.go | 11 ++ management/server/http/server.go | 6 +- management/server/peer.go | 184 ++++++++++++++++++++ management/server/store.go | 6 +- 8 files changed, 382 insertions(+), 174 deletions(-) create mode 100644 management/server/peer.go diff --git a/management/server/account.go b/management/server/account.go index 2990ef210..234bc8313 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -6,7 +6,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "net" - "strings" "sync" "time" ) @@ -25,17 +24,6 @@ type Account struct { Peers map[string]*Peer } -// Peer represents a machine connected to the network. -// The Peer is a Wireguard peer identified by a public key -type Peer struct { - // Wireguard public key - Key string - // A setup key this peer was registered with - SetupKey string - // IP address of the Peer - IP net.IP -} - // NewManager creates a new AccountManager with a provided Store func NewManager(store Store) *AccountManager { return &AccountManager{ @@ -44,40 +32,6 @@ func NewManager(store Store) *AccountManager { } } -// GetPeer returns a peer from a Store -func (manager *AccountManager) GetPeer(peerKey string) (*Peer, error) { - manager.mux.Lock() - defer manager.mux.Unlock() - - peer, err := manager.Store.GetPeer(peerKey) - if err != nil { - return nil, err - } - - return peer, nil -} - -// GetPeersForAPeer returns a list of peers available for a given peer (key) -// Effectively all the peers of the original peer's account except for the peer itself -func (manager *AccountManager) GetPeersForAPeer(peerKey string) ([]*Peer, error) { - manager.mux.Lock() - defer manager.mux.Unlock() - - account, err := manager.Store.GetPeerAccount(peerKey) - if err != nil { - return nil, status.Errorf(codes.Internal, "Invalid peer key %s", peerKey) - } - - var res []*Peer - for _, peer := range account.Peers { - if peer.Key != peerKey { - res = append(res, peer) - } - } - - return res, nil -} - //AddSetupKey generates a new setup key with a given name and type, and adds it to the specified account func (manager *AccountManager) AddSetupKey(accountId string, keyName string, keyType SetupKeyType, expiresIn time.Duration) (*SetupKey, error) { manager.mux.Lock() @@ -228,65 +182,6 @@ func (manager *AccountManager) createAccount(accountId string) (*Account, error) return account, nil } -// AddPeer adds a new peer to the Store. -// Each Account has a list of pre-authorised SetupKey and if no Account has a given key err wit ha code codes.Unauthenticated -// will be returned, meaning the key is invalid -// Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused). -// If the specified setupKey is empty then a new Account will be created //todo remove this part -func (manager *AccountManager) AddPeer(setupKey string, peerKey string) (*Peer, error) { - manager.mux.Lock() - defer manager.mux.Unlock() - - upperKey := strings.ToUpper(setupKey) - - var account *Account - var err error - var sk *SetupKey - if len(upperKey) == 0 { - // Empty setup key, create a new account for it. - account, sk = newAccount() - } else { - account, err = manager.Store.GetAccountBySetupKey(upperKey) - if err != nil { - return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", upperKey) - } - - sk = getAccountSetupKeyByKey(account, upperKey) - if sk == nil { - // shouldn't happen actually - return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", upperKey) - } - } - - if !sk.IsValid() { - return nil, status.Errorf(codes.FailedPrecondition, "setup key was expired or overused %s", upperKey) - } - - var takenIps []net.IP - for _, peer := range account.Peers { - takenIps = append(takenIps, peer.IP) - } - - network := account.Network - nextIp, _ := AllocatePeerIP(network.Net, takenIps) - - newPeer := &Peer{ - Key: peerKey, - SetupKey: sk.Key, - IP: nextIp, - } - - account.Peers[newPeer.Key] = newPeer - account.SetupKeys[sk.Key] = sk.IncrementUsage() - err = manager.Store.SaveAccount(account) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed adding peer") - } - - return newPeer, nil - -} - // newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id func newAccountWithId(accountId string) (*Account, *SetupKey) { diff --git a/management/server/file_store.go b/management/server/file_store.go index 0be51e558..1f3442695 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -83,6 +83,51 @@ func (s *FileStore) persist(file string) error { return util.WriteJson(file, s) } +// SavePeer saves updated peer +func (s *FileStore) SavePeer(accountId string, peer *Peer) error { + s.mux.Lock() + defer s.mux.Unlock() + + account, err := s.GetAccount(accountId) + if err != nil { + return err + } + + account.Peers[peer.Key] = peer + err = s.persist(s.storeFile) + if err != nil { + return err + } + + return nil +} + +// DeletePeer deletes peer from the Store +func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error) { + s.mux.Lock() + defer s.mux.Unlock() + + account, err := s.GetAccount(accountId) + if err != nil { + return nil, err + } + + peer := account.Peers[peerKey] + if peer == nil { + return nil, status.Errorf(codes.NotFound, "peer not found") + } + + delete(account.Peers, peerKey) + delete(s.PeerKeyId2AccountId, peerKey) + + err = s.persist(s.storeFile) + if err != nil { + return nil, err + } + + return peer, err +} + // GetPeer returns a peer from a Store func (s *FileStore) GetPeer(peerKey string) (*Peer, error) { s.mux.Lock() diff --git a/management/server/http/handler/peers.go b/management/server/http/handler/peers.go index b461ce4b6..5bf9cf471 100644 --- a/management/server/http/handler/peers.go +++ b/management/server/http/handler/peers.go @@ -2,24 +2,30 @@ package handler import ( "encoding/json" + "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "github.com/wiretrustee/wiretrustee/management/server" "net/http" "time" ) -// Peers is a handler that returns peers of the account +//Peers is a handler that returns peers of the account type Peers struct { accountManager *server.AccountManager } -// PeerResponse is a response sent to the client +//PeerResponse is a response sent to the client type PeerResponse struct { Name string IP string Connected bool LastSeen time.Time - Os string + OS string +} + +//PeerRequest is a request sent by the client +type PeerRequest struct { + Name string } func NewPeers(accountManager *server.AccountManager) *Peers { @@ -28,6 +34,63 @@ func NewPeers(accountManager *server.AccountManager) *Peers { } } +func (h *Peers) updatePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) { + req := &PeerRequest{} + peerIp := peer.IP + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + peer, err = h.accountManager.RenamePeer(accountId, peer.Key, req.Name) + if err != nil { + log.Errorf("failed updating peer %s under account %s %v", peerIp, accountId, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + writeJSONObject(w, toPeerResponse(peer)) +} +func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) { + _, err := h.accountManager.DeletePeer(accountId, peer.Key) + if err != nil { + log.Errorf("failed deleteing peer %s, %v", peer.IP, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } +} + +func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { + accountId := extractAccountIdFromRequestContext(r) + vars := mux.Vars(r) + peerId := vars["id"] //effectively peer IP address + if len(peerId) == 0 { + http.Error(w, "invalid peer Id", http.StatusBadRequest) + return + } + + peer, err := h.accountManager.GetPeerByIP(accountId, peerId) + if err != nil { + http.Error(w, "peer not found", http.StatusNotFound) + return + } + + switch r.Method { + case http.MethodDelete: + h.deletePeer(accountId, peer, w, r) + return + case http.MethodPut: + h.updatePeer(accountId, peer, w, r) + return + case http.MethodGet: + writeJSONObject(w, toPeerResponse(peer)) + return + + default: + http.Error(w, "", http.StatusNotFound) + } + +} + func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: @@ -39,27 +102,24 @@ func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusInternalServerError) return } - w.WriteHeader(200) - w.Header().Set("Content-Type", "application/json") respBody := []*PeerResponse{} for _, peer := range account.Peers { - respBody = append(respBody, &PeerResponse{ - Name: peer.Key, - IP: peer.IP.String(), - LastSeen: time.Now(), - Connected: false, - Os: "Ubuntu 21.04 (Hirsute Hippo)", - }) - } - - err = json.NewEncoder(w).Encode(respBody) - if err != nil { - log.Errorf("failed encoding account peers %s: %v", accountId, err) - http.Redirect(w, r, "/", http.StatusInternalServerError) - return + respBody = append(respBody, toPeerResponse(peer)) } + writeJSONObject(w, respBody) + return default: http.Error(w, "", http.StatusNotFound) } } + +func toPeerResponse(peer *server.Peer) *PeerResponse { + return &PeerResponse{ + Name: peer.Name, + IP: peer.IP.String(), + Connected: peer.Connected, + LastSeen: peer.LastSeen, + OS: peer.OS, + } +} diff --git a/management/server/http/handler/setupkeys.go b/management/server/http/handler/setupkeys.go index 1e84efad5..bbe07e8cc 100644 --- a/management/server/http/handler/setupkeys.go +++ b/management/server/http/handler/setupkeys.go @@ -43,8 +43,52 @@ func NewSetupKeysHandler(accountManager *server.AccountManager) *SetupKeys { } } -func (h *SetupKeys) CreateKey(w http.ResponseWriter, r *http.Request) { - accountId := extractAccountIdFromRequestContext(r) +func (h *SetupKeys) updateKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) { + req := &SetupKeyRequest{} + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var key *server.SetupKey + if req.Revoked { + //handle only if being revoked, don't allow to enable key again for now + key, err = h.accountManager.RevokeSetupKey(accountId, keyId) + if err != nil { + http.Error(w, "failed revoking key", http.StatusInternalServerError) + return + } + } + if len(req.Name) != 0 { + key, err = h.accountManager.RenameSetupKey(accountId, keyId, req.Name) + if err != nil { + http.Error(w, "failed renaming key", http.StatusInternalServerError) + return + } + } + + if key != nil { + writeSuccess(w, key) + } +} + +func (h *SetupKeys) getKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) { + account, err := h.accountManager.GetAccount(accountId) + if err != nil { + http.Error(w, "account doesn't exist", http.StatusInternalServerError) + return + } + for _, key := range account.SetupKeys { + if key.Id == keyId { + writeSuccess(w, key) + return + } + } + http.Error(w, "setup key not found", http.StatusNotFound) +} + +func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.Request) { req := &SetupKeyRequest{} err := json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -81,50 +125,11 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) { } switch r.Method { - case http.MethodPost: - req := &SetupKeyRequest{} - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - var key *server.SetupKey - if req.Revoked { - //handle only if being revoked, don't allow to enable key again for now - key, err = h.accountManager.RevokeSetupKey(accountId, keyId) - if err != nil { - http.Error(w, "failed revoking key", http.StatusInternalServerError) - return - } - } - if len(req.Name) != 0 { - key, err = h.accountManager.RenameSetupKey(accountId, keyId, req.Name) - if err != nil { - http.Error(w, "failed renaming key", http.StatusInternalServerError) - return - } - } - - if key != nil { - writeSuccess(w, key) - } - + case http.MethodPut: + h.updateKey(accountId, keyId, w, r) return - case http.MethodGet: - account, err := h.accountManager.GetAccount(accountId) - if err != nil { - http.Error(w, "account doesn't exist", http.StatusInternalServerError) - return - } - for _, key := range account.SetupKeys { - if key.Id == keyId { - writeSuccess(w, key) - return - } - } - http.Error(w, "setup key not found", http.StatusNotFound) + h.getKey(accountId, keyId, w, r) return default: http.Error(w, "", http.StatusNotFound) @@ -132,9 +137,15 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) { } func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) { + + accountId := extractAccountIdFromRequestContext(r) + switch r.Method { + case http.MethodPost: + h.createKey(accountId, w, r) + return case http.MethodGet: - accountId := extractAccountIdFromRequestContext(r) + //new user -> create a new account account, err := h.accountManager.GetOrCreateAccount(accountId) if err != nil { diff --git a/management/server/http/handler/util.go b/management/server/http/handler/util.go index b479cd8b8..b36667706 100644 --- a/management/server/http/handler/util.go +++ b/management/server/http/handler/util.go @@ -17,6 +17,17 @@ func extractAccountIdFromRequestContext(r *http.Request) string { return claims["sub"].(string) } +//writeJSONObject simply writes object to the HTTP reponse in JSON format +func writeJSONObject(w http.ResponseWriter, obj interface{}) { + w.WriteHeader(200) + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + err := json.NewEncoder(w).Encode(obj) + if err != nil { + http.Error(w, "failed handling request", http.StatusInternalServerError) + return + } +} + //Duration is used strictly for JSON requests/responses due to duration marshalling issues type Duration struct { time.Duration diff --git a/management/server/http/server.go b/management/server/http/server.go index 3de780182..828a819d5 100644 --- a/management/server/http/server.go +++ b/management/server/http/server.go @@ -62,10 +62,10 @@ func (s *Server) Start() error { peersHandler := handler.NewPeers(s.accountManager) keysHandler := handler.NewSetupKeysHandler(s.accountManager) r.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS") + r.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).Methods("GET", "PUT", "DELETE", "OPTIONS") - r.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "OPTIONS") - r.HandleFunc("/api/setup-keys", keysHandler.CreateKey).Methods("PUT") - r.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "POST", "OPTIONS") + r.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS") + r.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "PUT", "OPTIONS") http.Handle("/", r) if s.certManager != nil { diff --git a/management/server/peer.go b/management/server/peer.go new file mode 100644 index 000000000..e395cacfc --- /dev/null +++ b/management/server/peer.go @@ -0,0 +1,184 @@ +package server + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "net" + "strings" + "time" +) + +//Peer represents a machine connected to the network. +//The Peer is a Wireguard peer identified by a public key +type Peer struct { + //Wireguard public key + Key string + //A setup key this peer was registered with + SetupKey string + //IP address of the Peer + IP net.IP + //OS is peer's operating system + OS string + //Name is peer's name (machine name) + Name string + //LastSeen is the last time peer was connected to the management service + LastSeen time.Time + //Connected indicates whether peer is connected to the management service or not + Connected bool +} + +//Copy copies Peer object +func (p *Peer) Copy() *Peer { + return &Peer{ + Key: p.Key, + SetupKey: p.SetupKey, + IP: p.IP, + OS: p.OS, + Name: p.Name, + LastSeen: p.LastSeen, + Connected: p.Connected, + } +} + +//GetPeer returns a peer from a Store +func (manager *AccountManager) GetPeer(peerKey string) (*Peer, error) { + manager.mux.Lock() + defer manager.mux.Unlock() + + peer, err := manager.Store.GetPeer(peerKey) + if err != nil { + return nil, err + } + + return peer, nil +} + +//RenamePeer changes peer's name +func (manager *AccountManager) RenamePeer(accountId string, peerKey string, newName string) (*Peer, error) { + manager.mux.Lock() + defer manager.mux.Unlock() + + peer, err := manager.Store.GetPeer(peerKey) + if err != nil { + return nil, err + } + + peerCopy := peer.Copy() + peerCopy.Name = newName + err = manager.Store.SavePeer(accountId, peerCopy) + if err != nil { + return nil, err + } + + return peerCopy, nil +} + +//DeletePeer removes peer from the account by it's IP +func (manager *AccountManager) DeletePeer(accountId string, peerKey string) (*Peer, error) { + manager.mux.Lock() + defer manager.mux.Unlock() + return manager.Store.DeletePeer(accountId, peerKey) +} + +//GetPeerByIP returns peer by it's IP +func (manager *AccountManager) GetPeerByIP(accountId string, peerIP string) (*Peer, error) { + manager.mux.Lock() + defer manager.mux.Unlock() + + account, err := manager.Store.GetAccount(accountId) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + for _, peer := range account.Peers { + if peerIP == peer.IP.String() { + return peer, nil + } + } + + return nil, status.Errorf(codes.NotFound, "peer with IP %s not found", peerIP) +} + +// GetPeersForAPeer returns a list of peers available for a given peer (key) +// Effectively all the peers of the original peer's account except for the peer itself +func (manager *AccountManager) GetPeersForAPeer(peerKey string) ([]*Peer, error) { + manager.mux.Lock() + defer manager.mux.Unlock() + + account, err := manager.Store.GetPeerAccount(peerKey) + if err != nil { + return nil, status.Errorf(codes.Internal, "Invalid peer key %s", peerKey) + } + + var res []*Peer + for _, peer := range account.Peers { + if peer.Key != peerKey { + res = append(res, peer) + } + } + + return res, nil +} + +// AddPeer adds a new peer to the Store. +// Each Account has a list of pre-authorised SetupKey and if no Account has a given key err wit ha code codes.Unauthenticated +// will be returned, meaning the key is invalid +// Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused). +// If the specified setupKey is empty then a new Account will be created //todo remove this part +func (manager *AccountManager) AddPeer(setupKey string, peerKey string) (*Peer, error) { + manager.mux.Lock() + defer manager.mux.Unlock() + + upperKey := strings.ToUpper(setupKey) + + var account *Account + var err error + var sk *SetupKey + if len(upperKey) == 0 { + // Empty setup key, create a new account for it. + account, sk = newAccount() + } else { + account, err = manager.Store.GetAccountBySetupKey(upperKey) + if err != nil { + return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", upperKey) + } + + sk = getAccountSetupKeyByKey(account, upperKey) + if sk == nil { + // shouldn't happen actually + return nil, status.Errorf(codes.NotFound, "unknown setupKey %s", upperKey) + } + } + + if !sk.IsValid() { + return nil, status.Errorf(codes.FailedPrecondition, "setup key was expired or overused %s", upperKey) + } + + var takenIps []net.IP + for _, peer := range account.Peers { + takenIps = append(takenIps, peer.IP) + } + + network := account.Network + nextIp, _ := AllocatePeerIP(network.Net, takenIps) + + newPeer := &Peer{ + Key: peerKey, + SetupKey: sk.Key, + IP: nextIp, + OS: "todo", + Name: "todo", + LastSeen: time.Now(), + Connected: true, + } + + account.Peers[newPeer.Key] = newPeer + account.SetupKeys[sk.Key] = sk.IncrementUsage() + err = manager.Store.SaveAccount(account) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed adding peer") + } + + return newPeer, nil + +} diff --git a/management/server/store.go b/management/server/store.go index ca5305d61..84cf75c82 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -1,9 +1,11 @@ package server type Store interface { - GetPeer(peerId string) (*Peer, error) + GetPeer(peerKey string) (*Peer, error) + DeletePeer(accountId string, peerKey string) (*Peer, error) + SavePeer(accountId string, peer *Peer) error GetAccount(accountId string) (*Account, error) - GetPeerAccount(peerId string) (*Account, error) + GetPeerAccount(peerKey string) (*Account, error) GetAccountBySetupKey(setupKey string) (*Account, error) SaveAccount(account *Account) error }