From ad70200d92e4f45dc77c264ada0e87fbbaa3b5d1 Mon Sep 17 00:00:00 2001 From: Henry Barreto Date: Thu, 2 Mar 2023 15:19:35 -0300 Subject: [PATCH] agent,api,gateway,pkg,ssh: add public url to access HTTP application on device --- agent/main.go | 50 ++++++++++++++++++++++++++ agent/pkg/tunnel/tunnel.go | 7 ++++ agent/server/server.go | 11 ++++++ api/pkg/guard/actions.go | 23 ++++++------ api/pkg/guard/permissions.go | 8 +++++ api/routes/device.go | 63 +++++++++++++++++++++++++++------ api/server.go | 2 ++ api/services/device.go | 19 ++++++++++ api/store/device_store.go | 1 + api/store/mongo/device_store.go | 6 ++++ gateway/shellhub.conf | 13 +++++++ pkg/api/client/client.go | 12 +++---- pkg/api/client/client_public.go | 21 +++++++++-- pkg/api/request/device.go | 9 +++++ pkg/api/response/device.go | 5 +++ pkg/models/device.go | 1 + ssh/main.go | 29 +++++++++++++++ 17 files changed, 250 insertions(+), 30 deletions(-) create mode 100644 pkg/api/response/device.go diff --git a/agent/main.go b/agent/main.go index f2c2aee412e..13c756f4d24 100644 --- a/agent/main.go +++ b/agent/main.go @@ -1,9 +1,11 @@ package main import ( + "bufio" "fmt" "net" "net/http" + "net/url" "os" "runtime" "strings" @@ -170,6 +172,54 @@ func NewAgentServer() *Agent { conn.Close() } + tun.HTTPHandler = func(w http.ResponseWriter, r *http.Request) { + ok, err := serv.CheckDevicePublic() + if err != nil { + http.Error(w, "failed to check if the device is allowing HTTP connections", http.StatusInternalServerError) + + return + } + + if !ok { + http.Error(w, "the device is not allowing HTTP connections", http.StatusForbidden) + + return + } + + conn, err := net.Dial("tcp", ":8080") + if err != nil { + http.Error(w, "failed to connect to HTTP the server on device", http.StatusInternalServerError) + + return + } + + req := r.Clone(r.Context()) + req.URL.Scheme = "http" + req.URL.Path = r.Header.Get("X-Path") + req.URL.RawQuery = url.QueryEscape(r.Header.Get("X-Path")) + // TODO: check if the rest of url parameters are correct! + + if err := req.Write(conn); err != nil { + http.Error(w, "failed to write HTTP request to the server on device", http.StatusInternalServerError) + + return + } + + res, err := http.ReadResponse(bufio.NewReader(conn), req) + if err != nil { + http.Error(w, "failed to read HTTP response from the server on device", http.StatusInternalServerError) + + return + } + + if err := res.Write(w); err != nil { + http.Error(w, "failed to write HTTP response to the client", http.StatusInternalServerError) + + return + } + + conn.Close() + } tun.CloseHandler = func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) serv.CloseSession(vars["id"]) diff --git a/agent/pkg/tunnel/tunnel.go b/agent/pkg/tunnel/tunnel.go index 55909b68cd9..9ad96c35e08 100644 --- a/agent/pkg/tunnel/tunnel.go +++ b/agent/pkg/tunnel/tunnel.go @@ -12,6 +12,7 @@ import ( type Tunnel struct { router *mux.Router srv *http.Server + HTTPHandler func(w http.ResponseWriter, r *http.Request) ConnHandler func(w http.ResponseWriter, r *http.Request) CloseHandler func(w http.ResponseWriter, r *http.Request) } @@ -27,6 +28,9 @@ func NewTunnel() *Tunnel { return context.WithValue(ctx, "http-conn", c) //nolint:revive }, }, + HTTPHandler: func(w http.ResponseWriter, r *http.Request) { + panic("HTTPHandler can not be nil") + }, ConnHandler: func(w http.ResponseWriter, r *http.Request) { panic("connHandler can not be nil") }, @@ -34,6 +38,9 @@ func NewTunnel() *Tunnel { panic("closeHandler can not be nil") }, } + t.router.HandleFunc("/ssh/http", func(w http.ResponseWriter, r *http.Request) { + t.HTTPHandler(w, r) + }) t.router.HandleFunc("/ssh/{id}", func(w http.ResponseWriter, r *http.Request) { t.ConnHandler(w, r) }) diff --git a/agent/server/server.go b/agent/server/server.go index eafdffc9042..fa36a1580f2 100644 --- a/agent/server/server.go +++ b/agent/server/server.go @@ -57,6 +57,17 @@ type Server struct { singleUserPassword string } +func (s *Server) CheckDevicePublic() (bool, error) { + enabled, err := s.api.CheckDevicePublicURL(s.authData.UID, s.authData.Token) + if err != nil { + log.WithError(err).Error("Failed to check device public") + + return false, err + } + + return enabled, nil +} + // NewServer creates a new server SSH agent server. func NewServer(api client.Client, authData *models.DeviceAuthResponse, privateKey string, keepAliveInterval int, singleUserPassword string) *Server { server := &Server{ diff --git a/api/pkg/guard/actions.go b/api/pkg/guard/actions.go index 031ad8c7a0d..9e9d403ef1b 100644 --- a/api/pkg/guard/actions.go +++ b/api/pkg/guard/actions.go @@ -13,7 +13,7 @@ type AllActions struct { } type DeviceActions struct { - Accept, Reject, Remove, Connect, Rename, CreateTag, UpdateTag, RemoveTag, RenameTag, DeleteTag int + Accept, Reject, Remove, Connect, Rename, CreateTag, UpdateTag, RemoveTag, RenameTag, DeleteTag, UpdatePublicURL int } type SessionActions struct { @@ -40,16 +40,17 @@ type BillingActions struct { // You should use it to get the code's action. var Actions = AllActions{ Device: DeviceActions{ - Accept: DeviceAccept, - Reject: DeviceReject, - Remove: DeviceRemove, - Connect: DeviceConnect, - Rename: DeviceRename, - CreateTag: DeviceCreateTag, - UpdateTag: DeviceUpdateTag, - RemoveTag: DeviceRemoveTag, - RenameTag: DeviceRenameTag, - DeleteTag: DeviceDeleteTag, + Accept: DeviceAccept, + Reject: DeviceReject, + Remove: DeviceRemove, + Connect: DeviceConnect, + Rename: DeviceRename, + CreateTag: DeviceCreateTag, + UpdateTag: DeviceUpdateTag, + RemoveTag: DeviceRemoveTag, + RenameTag: DeviceRenameTag, + DeleteTag: DeviceDeleteTag, + UpdatePublicURL: DeviceUpdatePublicURL, }, Session: SessionActions{ Play: SessionPlay, diff --git a/api/pkg/guard/permissions.go b/api/pkg/guard/permissions.go index 60df39480d4..e3e0aa39dd9 100644 --- a/api/pkg/guard/permissions.go +++ b/api/pkg/guard/permissions.go @@ -16,6 +16,8 @@ const ( DeviceRenameTag DeviceDeleteTag + DeviceUpdatePublicURL + SessionPlay SessionClose SessionRemove @@ -73,6 +75,8 @@ var operatorPermissions = Permissions{ DeviceRenameTag, DeviceDeleteTag, + DeviceUpdatePublicURL, + SessionDetails, } @@ -90,6 +94,8 @@ var adminPermissions = Permissions{ DeviceRenameTag, DeviceDeleteTag, + DeviceUpdatePublicURL, + SessionPlay, SessionClose, SessionRemove, @@ -130,6 +136,8 @@ var ownerPermissions = Permissions{ DeviceRenameTag, DeviceDeleteTag, + DeviceUpdatePublicURL, + SessionPlay, SessionClose, SessionRemove, diff --git a/api/routes/device.go b/api/routes/device.go index 99cac8ff970..e2fd7b8a081 100644 --- a/api/routes/device.go +++ b/api/routes/device.go @@ -10,21 +10,23 @@ import ( "github.com/shellhub-io/shellhub/api/pkg/guard" "github.com/shellhub-io/shellhub/pkg/api/paginator" "github.com/shellhub-io/shellhub/pkg/api/request" + "github.com/shellhub-io/shellhub/pkg/api/response" "github.com/shellhub-io/shellhub/pkg/models" ) const ( - GetDeviceListURL = "/devices" - GetDeviceURL = "/devices/:uid" - DeleteDeviceURL = "/devices/:uid" - RenameDeviceURL = "/devices/:uid" - OfflineDeviceURL = "/devices/:uid/offline" - HeartbeatDeviceURL = "/devices/:uid/heartbeat" - LookupDeviceURL = "/lookup" - UpdateStatusURL = "/devices/:uid/:status" - CreateTagURL = "/devices/:uid/tags" // Add a tag to a device. - UpdateTagURL = "/devices/:uid/tags" // Update device's tags with a new set. - RemoveTagURL = "/devices/:uid/tags/:tag" // Delete a tag from a device. + GetDeviceListURL = "/devices" + GetDeviceURL = "/devices/:uid" + DeleteDeviceURL = "/devices/:uid" + RenameDeviceURL = "/devices/:uid" + OfflineDeviceURL = "/devices/:uid/offline" + HeartbeatDeviceURL = "/devices/:uid/heartbeat" + LookupDeviceURL = "/lookup" + UpdateStatusURL = "/devices/:uid/:status" + CreateTagURL = "/devices/:uid/tags" // Add a tag to a device. + UpdateTagURL = "/devices/:uid/tags" // Update device's tags with a new set. + RemoveTagURL = "/devices/:uid/tags/:tag" // Delete a tag from a device. + CheckDevicePublicURL = "/devices/:uid/public" // Check if a device allows public access. ) const ( @@ -281,3 +283,42 @@ func (h *Handler) UpdateDeviceTag(c gateway.Context) error { return c.NoContent(http.StatusOK) } + +func (h *Handler) UpdateDevicePublicURL(c gateway.Context) error { + var req request.DeviceUpdatePublicURL + if err := c.Bind(&req); err != nil { + return err + } + + if err := c.Validate(&req); err != nil { + return err + } + + if err := guard.EvaluatePermission(c.Role(), guard.Actions.Device.UpdatePublicURL, func() error { + return h.service.UpdateDevicePublicURL(c.Ctx(), models.UID(req.UID), req.PublicURL) + }); err != nil { + return err + } + + return c.NoContent(http.StatusOK) +} + +func (h *Handler) GetDevicePublicURL(c gateway.Context) error { + var req request.DeviceGetPublicURL + if err := c.Bind(&req); err != nil { + return err + } + + if err := c.Validate(&req); err != nil { + return err + } + + status, err := h.service.GetDevicePublicURL(c.Ctx(), models.UID(req.UID)) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, response.DeviceGetPublicURL{ + PublicURL: status, + }) +} diff --git a/api/server.go b/api/server.go index e6a55849cee..40573148325 100644 --- a/api/server.go +++ b/api/server.go @@ -216,6 +216,8 @@ func startServer(cfg *config) error { publicAPI.DELETE(routes.RemoveTagURL, gateway.Handler(handler.RemoveDeviceTag)) publicAPI.PUT(routes.UpdateTagURL, gateway.Handler(handler.UpdateDeviceTag)) + publicAPI.GET(routes.CheckDevicePublicURL, gateway.Handler(handler.GetDevicePublicURL)) + publicAPI.GET(routes.GetTagsURL, gateway.Handler(handler.GetTags)) publicAPI.PUT(routes.RenameTagURL, gateway.Handler(handler.RenameTag)) publicAPI.DELETE(routes.DeleteTagsURL, gateway.Handler(handler.DeleteTag)) diff --git a/api/services/device.go b/api/services/device.go index c18f073b4ca..5a43983d8a0 100644 --- a/api/services/device.go +++ b/api/services/device.go @@ -25,6 +25,8 @@ type DeviceService interface { UpdatePendingStatus(ctx context.Context, uid models.UID, status, tenant string) error SetDevicePosition(ctx context.Context, uid models.UID, ip string) error DeviceHeartbeat(ctx context.Context, uid models.UID) error + GetDevicePublicURL(ctx context.Context, uid models.UID) (bool, error) + UpdateDevicePublicURL(ctx context.Context, uid models.UID, publicURL bool) error } func (s *service) ListDevices(ctx context.Context, pagination paginator.Query, filter []models.Filter, status string, sort string, order string) ([]models.Device, int, error) { @@ -225,3 +227,20 @@ func (s *service) DeviceHeartbeat(ctx context.Context, uid models.UID) error { return nil } + +func (s *service) GetDevicePublicURL(ctx context.Context, uid models.UID) (bool, error) { + device, err := s.store.DeviceGet(ctx, uid) + if err != nil { + return false, NewErrDeviceNotFound(uid, err) + } + + return device.PublicURL, nil +} + +func (s *service) UpdateDevicePublicURL(ctx context.Context, uid models.UID, publicURL bool) error { + if err := s.store.DeviceSetPublicURL(ctx, uid, publicURL); err != nil { + return NewErrDeviceNotFound(uid, err) + } + + return nil +} diff --git a/api/store/device_store.go b/api/store/device_store.go index b064c61ced8..aa10da3e4fb 100644 --- a/api/store/device_store.go +++ b/api/store/device_store.go @@ -25,4 +25,5 @@ type DeviceStore interface { DeviceSetPosition(ctx context.Context, uid models.UID, position models.DevicePosition) error DeviceListByUsage(ctx context.Context, tenantID string) ([]models.UID, error) DeviceChooser(ctx context.Context, tenantID string, chosen []string) error + DeviceSetPublicURL(ctx context.Context, uid models.UID, publicURL bool) error } diff --git a/api/store/mongo/device_store.go b/api/store/mongo/device_store.go index 18a1905b9b1..d98030c3e59 100644 --- a/api/store/mongo/device_store.go +++ b/api/store/mongo/device_store.go @@ -443,3 +443,9 @@ func (s *Store) DeviceChooser(ctx context.Context, tenantID string, chosen []str return nil } + +func (s *Store) DeviceSetPublicURL(ctx context.Context, uid models.UID, publicURL bool) error { + _, err := s.db.Collection("devices").UpdateOne(ctx, bson.M{"uid": uid}, bson.M{"$set": bson.M{"public_url": publicURL}}) + + return FromMongoError(err) +} diff --git a/gateway/shellhub.conf b/gateway/shellhub.conf index 30eebaef0a0..ebb5080d1a4 100644 --- a/gateway/shellhub.conf +++ b/gateway/shellhub.conf @@ -391,3 +391,16 @@ server { } } } + +server { + listen 80; + server_name ~^(?.+)\.(?.+)\..+$; + + location / { #~ ^/(.*)$ { + rewrite ^/(.*)$ /ssh/http break; + proxy_set_header X-Namespace $namespace; + proxy_set_header X-Device $device; + proxy_set_header X-Path /$1$is_args$args; + proxy_pass http://ssh:8080; + } +} \ No newline at end of file diff --git a/pkg/api/client/client.go b/pkg/api/client/client.go index de943318b78..c31b02c2c59 100644 --- a/pkg/api/client/client.go +++ b/pkg/api/client/client.go @@ -67,6 +67,12 @@ type client struct { logger *logrus.Logger } +func buildURL(c *client, uri string) string { + u, _ := url.Parse(fmt.Sprintf("%s://%s:%d%s", c.scheme, c.host, c.port, uri)) + + return u.String() +} + func (c *client) ListDevices() ([]models.Device, error) { list := []models.Device{} _, err := c.http.R(). @@ -97,9 +103,3 @@ func (c *client) GetDevice(uid string) (*models.Device, error) { return nil, ErrUnknown } } - -func buildURL(c *client, uri string) string { - u, _ := url.Parse(fmt.Sprintf("%s://%s:%d%s", c.scheme, c.host, c.port, uri)) - - return u.String() -} diff --git a/pkg/api/client/client_public.go b/pkg/api/client/client_public.go index 1429e165022..6bc5562b7b9 100644 --- a/pkg/api/client/client_public.go +++ b/pkg/api/client/client_public.go @@ -32,6 +32,7 @@ type publicAPI interface { AuthDevice(req *models.DeviceAuthRequest) (*models.DeviceAuthResponse, error) NewReverseListener(token string) (*revdial.Listener, error) AuthPublicKey(req *models.PublicKeyAuthRequest, token string) (*models.PublicKeyAuthResponse, error) + CheckDevicePublicURL(uid string, token string) (bool, error) } func (c *client) GetInfo(agentVersion string) (*models.Info, error) { @@ -89,6 +90,10 @@ func (c *client) Endpoints() (*models.Endpoints, error) { return endpoints, nil } +func tunnelDial(ctx context.Context, protocol, address string, port int, path string) (*websocket.Conn, *http.Response, error) { + return websocket.DefaultDialer.DialContext(ctx, strings.Join([]string{fmt.Sprintf("%s://%s:%d", protocol, address, port), path}, ""), nil) +} + func (c *client) NewReverseListener(token string) (*revdial.Listener, error) { req, _ := http.NewRequest("GET", "", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) @@ -122,6 +127,18 @@ func (c *client) AuthPublicKey(req *models.PublicKeyAuthRequest, token string) ( return res, nil } -func tunnelDial(ctx context.Context, protocol, address string, port int, path string) (*websocket.Conn, *http.Response, error) { - return websocket.DefaultDialer.DialContext(ctx, strings.Join([]string{fmt.Sprintf("%s://%s:%d", protocol, address, port), path}, ""), nil) +func (c *client) CheckDevicePublicURL(uid string, token string) (bool, error) { + status := struct { + PublicURL bool `json:"public_url"` + } {} + + _, err := c.http.R(). + SetResult(&status). + SetAuthToken(token). + Get(buildURL(c, fmt.Sprintf("/api/devices/%s/public", uid))) + if err != nil { + return false, err + } + + return status.PublicURL, nil // TODO: return status. } diff --git a/pkg/api/request/device.go b/pkg/api/request/device.go index 4cd214d54ef..171b8222477 100644 --- a/pkg/api/request/device.go +++ b/pkg/api/request/device.go @@ -89,3 +89,12 @@ type DeviceAuth struct { PublicKey string `json:"public_key" validate:"required"` TenantID string `json:"tenant_id" validate:"required"` } + +type DeviceGetPublicURL struct { + DeviceParam +} + +type DeviceUpdatePublicURL struct { + DeviceParam + PublicURL bool `json:"public_url"` +} diff --git a/pkg/api/response/device.go b/pkg/api/response/device.go new file mode 100644 index 00000000000..499519f5a88 --- /dev/null +++ b/pkg/api/response/device.go @@ -0,0 +1,5 @@ +package response + +type DeviceGetPublicURL struct { + PublicURL bool `json:"public_url"` +} diff --git a/pkg/models/device.go b/pkg/models/device.go index a2c96be2c68..98d4a0cbd54 100644 --- a/pkg/models/device.go +++ b/pkg/models/device.go @@ -22,6 +22,7 @@ type Device struct { RemoteAddr string `json:"remote_addr" bson:"remote_addr"` Position *DevicePosition `json:"position" bson:"position"` Tags []string `json:"tags" bson:"tags,omitempty"` + PublicURL bool `json:"public_url" bson:"public_url,omitempty"` } type DeviceAuthClaims struct { diff --git a/ssh/main.go b/ssh/main.go index 7e88a18ccc2..9b4fa899630 100644 --- a/ssh/main.go +++ b/ssh/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "context" "encoding/json" "fmt" @@ -70,6 +71,34 @@ func main() { return } }) + router.HandleFunc("/ssh/http", func(w http.ResponseWriter, r *http.Request) { + dev, err := tunnel.API.GetDevice("d6c6a5e97217bbe4467eae46ab004695a766c5c43f70b95efd4b6a4d32b33c6e") + if err != nil { + http.Error(w, fmt.Sprintf("failed to get device: %s", err), http.StatusInternalServerError) + + return + } + + conn, err := tunnel.Dial(context.Background(), dev.UID) + if err != nil { + http.Error(w, fmt.Sprintf("failed to connect to device: %s", err), http.StatusInternalServerError) + + return + } + + if err = r.Write(conn); err != nil { + http.Error(w, fmt.Sprintf("failed to write request to device: %s", err), http.StatusInternalServerError) + + return + } + + res, _ := http.ReadResponse(bufio.NewReader(conn), r) + if err = res.Write(w); err != nil { + http.Error(w, fmt.Sprintf("failed to write response to client: %s", err), http.StatusInternalServerError) + + return + } + }) // TODO: add `/ws/ssh` route to OpenAPI repository. router.Handle("/ws/ssh", web.HandlerRestoreSession(web.RestoreSession, handler.WebSession)).