mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-08 01:04:47 +01:00
966661fe91
This PR is a part of an effort to use standard ports (443 or 80) that are usually allowed by default in most of the environments. Right now Management Service runs the Let'sEncrypt manager on port 443, HTTP API server on port 33071, and a gRPC server on port 33073. There are three separate listeners. This PR combines these listeners into one. With this change, the HTTP and gRPC server runs on either 443 with TLS or 80 without TLS by default (no --port specified). Let's Encrypt manager always runs on port 443 if enabled. The backward compatibility server runs on port 33073 (with TLS or without). HTTP port 33071 is obsolete and not used anymore. Newly installed agents will connect to port 443 by default instead of port 33073 if not specified otherwise.
372 lines
9.7 KiB
Go
372 lines
9.7 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/netbirdio/netbird/management/server/http/api"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"net/http"
|
|
|
|
"github.com/netbirdio/netbird/management/server"
|
|
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
|
"github.com/rs/xid"
|
|
|
|
"github.com/gorilla/mux"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Groups is a handler that returns groups of the account
|
|
type Groups struct {
|
|
jwtExtractor jwtclaims.ClaimsExtractor
|
|
accountManager server.AccountManager
|
|
authAudience string
|
|
}
|
|
|
|
func NewGroups(accountManager server.AccountManager, authAudience string) *Groups {
|
|
return &Groups{
|
|
accountManager: accountManager,
|
|
authAudience: authAudience,
|
|
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
|
|
}
|
|
}
|
|
|
|
// GetAllGroupsHandler list for the account
|
|
func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
|
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
|
if err != nil {
|
|
log.Error(err)
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var groups []*api.Group
|
|
for _, g := range account.Groups {
|
|
groups = append(groups, toGroupResponse(account, g))
|
|
}
|
|
|
|
writeJSONObject(w, groups)
|
|
}
|
|
|
|
// UpdateGroupHandler handles update to a group identified by a given ID
|
|
func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
|
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
groupID, ok := vars["id"]
|
|
if !ok {
|
|
http.Error(w, "group ID field is missing", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if len(groupID) == 0 {
|
|
http.Error(w, "group ID can't be empty", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
|
|
_, ok = account.Groups[groupID]
|
|
if !ok {
|
|
http.Error(w, fmt.Sprintf("couldn't find group with ID %s", groupID), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
allGroup, err := account.GetGroupAll()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if allGroup.ID == groupID {
|
|
http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req api.PutApiGroupsIdJSONRequestBody
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if *req.Name == "" {
|
|
http.Error(w, "group name shouldn't be empty", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
|
|
group := server.Group{
|
|
ID: groupID,
|
|
Name: *req.Name,
|
|
Peers: peerIPsToKeys(account, req.Peers),
|
|
}
|
|
|
|
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
|
|
log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err)
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSONObject(w, toGroupResponse(account, &group))
|
|
}
|
|
|
|
// PatchGroupHandler handles patch updates to a group identified by a given ID
|
|
func (h *Groups) PatchGroupHandler(w http.ResponseWriter, r *http.Request) {
|
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
vars := mux.Vars(r)
|
|
groupID := vars["id"]
|
|
if len(groupID) == 0 {
|
|
http.Error(w, "invalid group Id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
_, ok := account.Groups[groupID]
|
|
if !ok {
|
|
http.Error(w, fmt.Sprintf("couldn't find group id %s", groupID), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
allGroup, err := account.GetGroupAll()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if allGroup.ID == groupID {
|
|
http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
var req api.PatchApiGroupsIdJSONRequestBody
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(req) == 0 {
|
|
http.Error(w, "no patch instruction received", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var operations []server.GroupUpdateOperation
|
|
|
|
for _, patch := range req {
|
|
switch patch.Path {
|
|
case api.GroupPatchOperationPathName:
|
|
if patch.Op != api.GroupPatchOperationOpReplace {
|
|
http.Error(w, fmt.Sprintf("Name field only accepts replace operation, got %s", patch.Op),
|
|
http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if len(patch.Value) == 0 || patch.Value[0] == "" {
|
|
http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
|
|
operations = append(operations, server.GroupUpdateOperation{
|
|
Type: server.UpdateGroupName,
|
|
Values: patch.Value,
|
|
})
|
|
case api.GroupPatchOperationPathPeers:
|
|
switch patch.Op {
|
|
case api.GroupPatchOperationOpReplace:
|
|
peerKeys := peerIPsToKeys(account, &patch.Value)
|
|
operations = append(operations, server.GroupUpdateOperation{
|
|
Type: server.UpdateGroupPeers,
|
|
Values: peerKeys,
|
|
})
|
|
case api.GroupPatchOperationOpRemove:
|
|
peerKeys := peerIPsToKeys(account, &patch.Value)
|
|
operations = append(operations, server.GroupUpdateOperation{
|
|
Type: server.RemovePeersFromGroup,
|
|
Values: peerKeys,
|
|
})
|
|
case api.GroupPatchOperationOpAdd:
|
|
peerKeys := peerIPsToKeys(account, &patch.Value)
|
|
operations = append(operations, server.GroupUpdateOperation{
|
|
Type: server.InsertPeersToGroup,
|
|
Values: peerKeys,
|
|
})
|
|
default:
|
|
http.Error(w, "invalid operation, \"%s\", for Peers field", http.StatusBadRequest)
|
|
return
|
|
}
|
|
default:
|
|
http.Error(w, "invalid patch path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
group, err := h.accountManager.UpdateGroup(account.Id, groupID, operations)
|
|
|
|
if err != nil {
|
|
errStatus, ok := status.FromError(err)
|
|
if ok && errStatus.Code() == codes.Internal {
|
|
http.Error(w, errStatus.String(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if ok && errStatus.Code() == codes.NotFound {
|
|
http.Error(w, errStatus.String(), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err)
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSONObject(w, toGroupResponse(account, group))
|
|
}
|
|
|
|
// CreateGroupHandler handles group creation request
|
|
func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) {
|
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var req api.PostApiGroupsJSONRequestBody
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.Name == "" {
|
|
http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity)
|
|
return
|
|
}
|
|
|
|
group := server.Group{
|
|
ID: xid.New().String(),
|
|
Name: req.Name,
|
|
Peers: peerIPsToKeys(account, req.Peers),
|
|
}
|
|
|
|
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
|
|
log.Errorf("failed creating group \"%s\" under account %s %v", req.Name, account.Id, err)
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSONObject(w, toGroupResponse(account, &group))
|
|
}
|
|
|
|
// DeleteGroupHandler handles group deletion request
|
|
func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
|
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
aID := account.Id
|
|
|
|
groupID := mux.Vars(r)["id"]
|
|
if len(groupID) == 0 {
|
|
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
allGroup, err := account.GetGroupAll()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if allGroup.ID == groupID {
|
|
http.Error(w, "deleting group ALL is not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
if err := h.accountManager.DeleteGroup(aID, groupID); err != nil {
|
|
log.Errorf("failed delete group %s under account %s %v", groupID, aID, err)
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSONObject(w, "")
|
|
}
|
|
|
|
// GetGroupHandler returns a group
|
|
func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
|
|
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
|
|
if err != nil {
|
|
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
groupID := mux.Vars(r)["id"]
|
|
if len(groupID) == 0 {
|
|
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
group, err := h.accountManager.GetGroup(account.Id, groupID)
|
|
if err != nil {
|
|
http.Error(w, "group not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
writeJSONObject(w, toGroupResponse(account, group))
|
|
default:
|
|
http.Error(w, "", http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func peerIPsToKeys(account *server.Account, peerIPs *[]string) []string {
|
|
var mappedPeerKeys []string
|
|
if peerIPs == nil {
|
|
return mappedPeerKeys
|
|
}
|
|
|
|
peersChecked := make(map[string]struct{})
|
|
|
|
for _, requestPeersIP := range *peerIPs {
|
|
_, ok := peersChecked[requestPeersIP]
|
|
if ok {
|
|
continue
|
|
}
|
|
peersChecked[requestPeersIP] = struct{}{}
|
|
for _, accountPeer := range account.Peers {
|
|
if accountPeer.IP.String() == requestPeersIP {
|
|
mappedPeerKeys = append(mappedPeerKeys, accountPeer.Key)
|
|
}
|
|
}
|
|
}
|
|
return mappedPeerKeys
|
|
}
|
|
|
|
func toGroupResponse(account *server.Account, group *server.Group) *api.Group {
|
|
cache := make(map[string]api.PeerMinimum)
|
|
gr := api.Group{
|
|
Id: group.ID,
|
|
Name: group.Name,
|
|
PeersCount: len(group.Peers),
|
|
}
|
|
|
|
for _, pid := range group.Peers {
|
|
_, ok := cache[pid]
|
|
if !ok {
|
|
peer, ok := account.Peers[pid]
|
|
if !ok {
|
|
continue
|
|
}
|
|
peerResp := api.PeerMinimum{
|
|
Id: peer.IP.String(),
|
|
Name: peer.Name,
|
|
}
|
|
cache[pid] = peerResp
|
|
gr.Peers = append(gr.Peers, peerResp)
|
|
}
|
|
}
|
|
return &gr
|
|
}
|