Skip to content

Commit e7b18d8

Browse files
committed
wip(api,ssh): private keys and sessions
1 parent 1ff378d commit e7b18d8

21 files changed

+418
-81
lines changed

api/routes/device.go

+39-40
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"strconv"
66

77
"github.com/shellhub-io/shellhub/api/pkg/gateway"
8-
"github.com/shellhub-io/shellhub/pkg/api/query"
98
"github.com/shellhub-io/shellhub/pkg/api/requests"
109
"github.com/shellhub-io/shellhub/pkg/models"
1110
)
@@ -44,45 +43,45 @@ func (h *Handler) GetDeviceList(c gateway.Context) error {
4443
return err
4544
}
4645

47-
if c.QueryParam("connector") != "" {
48-
filter := []query.Filter{
49-
{
50-
Type: query.FilterTypeProperty,
51-
Params: &query.FilterProperty{
52-
Name: "info.platform",
53-
Operator: "eq",
54-
Value: "connector",
55-
},
56-
},
57-
{
58-
Type: query.FilterTypeOperator,
59-
Params: &query.FilterOperator{
60-
Name: "and",
61-
},
62-
},
63-
}
64-
65-
req.Filters.Data = append(req.Filters.Data, filter...)
66-
} else {
67-
filter := []query.Filter{
68-
{
69-
Type: query.FilterTypeProperty,
70-
Params: &query.FilterProperty{
71-
Name: "info.platform",
72-
Operator: "ne",
73-
Value: "connector",
74-
},
75-
},
76-
{
77-
Type: query.FilterTypeOperator,
78-
Params: &query.FilterOperator{
79-
Name: "and",
80-
},
81-
},
82-
}
83-
84-
req.Filters.Data = append(req.Filters.Data, filter...)
85-
}
46+
// if c.QueryParam("connector") != "" {
47+
// filter := []query.Filter{
48+
// {
49+
// Type: query.FilterTypeProperty,
50+
// Params: &query.FilterProperty{
51+
// Name: "info.platform",
52+
// Operator: "eq",
53+
// Value: "connector",
54+
// },
55+
// },
56+
// {
57+
// Type: query.FilterTypeOperator,
58+
// Params: &query.FilterOperator{
59+
// Name: "and",
60+
// },
61+
// },
62+
// }
63+
//
64+
// req.Filters.Data = append(req.Filters.Data, filter...)
65+
// } else {
66+
// filter := []query.Filter{
67+
// {
68+
// Type: query.FilterTypeProperty,
69+
// Params: &query.FilterProperty{
70+
// Name: "info.platform",
71+
// Operator: "ne",
72+
// Value: "connector",
73+
// },
74+
// },
75+
// {
76+
// Type: query.FilterTypeOperator,
77+
// Params: &query.FilterOperator{
78+
// Name: "and",
79+
// },
80+
// },
81+
// }
82+
//
83+
// req.Filters.Data = append(req.Filters.Data, filter...)
84+
// }
8685

8786
if err := c.Validate(req); err != nil {
8887
return err

api/routes/session.go

+10
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ func (h *Handler) GetSession(c gateway.Context) error {
6262
}
6363

6464
func (h *Handler) UpdateSession(c gateway.Context) error {
65+
println("!2222222")
66+
println("!2222222")
67+
println("!2222222")
68+
println("!2222222")
69+
6570
var req requests.SessionUpdate
6671
if err := c.Bind(&req); err != nil {
6772
return err
@@ -79,6 +84,11 @@ func (h *Handler) UpdateSession(c gateway.Context) error {
7984
}
8085

8186
func (h *Handler) CreateSession(c gateway.Context) error {
87+
println("!1111111")
88+
println("!1111111")
89+
println("!1111111")
90+
println("!1111111")
91+
8292
var req requests.SessionCreate
8393
if err := c.Bind(&req); err != nil {
8494
return err

api/services/auth.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func (s *service) AuthDevice(ctx context.Context, req requests.DeviceAuth, remot
8888
}
8989

9090
uidSHA := sha256.Sum256(structhash.Dump(auth, 1))
91-
device, err := s.store.DeviceGet(ctx, models.UID(hex.EncodeToString(uidSHA[:])))
91+
device, err := s.store.DeviceGet(ctx, store.DeviceIdentID, hex.EncodeToString(uidSHA[:]))
9292
if err != nil {
9393
if err != store.ErrNoDocuments {
9494
return nil, err

api/services/device.go

+83-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package services
33
import (
44
"context"
55

6+
"github.com/shellhub-io/shellhub/api/store"
67
"github.com/shellhub-io/shellhub/pkg/api/requests"
8+
"github.com/shellhub-io/shellhub/pkg/envs"
79
"github.com/shellhub-io/shellhub/pkg/models"
810
)
911

@@ -22,11 +24,64 @@ type DeviceService interface {
2224
}
2325

2426
func (s *service) ListDevices(ctx context.Context, req *requests.DeviceList) ([]models.Device, int, error) {
25-
return nil, 0, nil
27+
// if req.DeviceStatus == models.DeviceStatusRemoved {
28+
// // TODO: unique DeviceList
29+
// removed, count, err := s.store.DeviceRemovedList(ctx, req.TenantID, req.Paginator, req.Filters, req.Sorter)
30+
// if err != nil {
31+
// return nil, 0, err
32+
// }
33+
//
34+
// devices := make([]models.Device, 0, len(removed))
35+
// for _, device := range removed {
36+
// devices = append(devices, *device.Device)
37+
// }
38+
//
39+
// return devices, count, nil
40+
// }
41+
//
42+
// if req.TenantID != "" {
43+
// ns, err := s.store.NamespaceGet(ctx, req.TenantID, s.store.Options().CountAcceptedDevices())
44+
// if err != nil {
45+
// return nil, 0, NewErrNamespaceNotFound(req.TenantID, err)
46+
// }
47+
//
48+
// if ns.HasMaxDevices() {
49+
// switch {
50+
// case envs.IsCloud():
51+
// removed, err := s.store.DeviceRemovedCount(ctx, ns.TenantID)
52+
// if err != nil {
53+
// return nil, 0, NewErrDeviceRemovedCount(err)
54+
// }
55+
//
56+
// if ns.HasLimitDevicesReached(removed) {
57+
// return s.store.DeviceList(ctx, req.DeviceStatus, req.Paginator, req.Filters, req.Sorter, store.DeviceAcceptableFromRemoved)
58+
// }
59+
// case envs.IsEnterprise():
60+
// fallthrough
61+
// case envs.IsCommunity():
62+
// if ns.HasMaxDevicesReached() {
63+
// return s.store.DeviceList(ctx, req.DeviceStatus, req.Paginator, req.Filters, req.Sorter, store.DeviceAcceptableAsFalse)
64+
// }
65+
// }
66+
// }
67+
// }
68+
69+
return s.store.DeviceList(
70+
ctx,
71+
s.store.Options().InNamespace(req.TenantID),
72+
s.store.Options().Filter(req.Filters),
73+
s.store.Options().Paginate(req.Paginator),
74+
s.store.Options().Order(req.Sorter),
75+
)
2676
}
2777

2878
func (s *service) GetDevice(ctx context.Context, uid models.UID) (*models.Device, error) {
29-
return nil, nil
79+
device, err := s.store.DeviceGet(ctx, store.DeviceIdentID, string(uid))
80+
if err != nil {
81+
return nil, NewErrDeviceNotFound(uid, err)
82+
}
83+
84+
return device, nil
3085
}
3186

3287
// DeleteDevice deletes a device from a namespace.
@@ -38,7 +93,26 @@ func (s *service) GetDevice(ctx context.Context, uid models.UID) (*models.Device
3893
// NewErrNamespaceNotFound(tenant, err), if the usage cannot be reported, ErrReport or if the store function that
3994
// delete the device fails.
4095
func (s *service) DeleteDevice(ctx context.Context, uid models.UID, tenant string) error {
41-
return nil
96+
ns, err := s.store.NamespaceGet(ctx, store.NamespaceIdentID, tenant)
97+
if err != nil {
98+
return NewErrNamespaceNotFound(tenant, err)
99+
}
100+
101+
device, err := s.store.DeviceGet(ctx, store.DeviceIdentID, string(uid))
102+
if err != nil {
103+
return NewErrDeviceNotFound(uid, err)
104+
}
105+
106+
// If the namespace has a limit of devices, we change the device's slot status to removed.
107+
// This way, we can keep track of the number of devices that were removed from the namespace and void the device
108+
// switching.
109+
if envs.IsCloud() && envs.HasBilling() && !ns.Billing.IsActive() {
110+
if err := s.store.DeviceRemovedInsert(ctx, tenant, device); err != nil {
111+
return NewErrDeviceRemovedInsert(err)
112+
}
113+
}
114+
115+
return s.store.DeviceDelete(ctx, uid)
42116
}
43117

44118
func (s *service) RenameDevice(ctx context.Context, uid models.UID, name, tenant string) error {
@@ -50,7 +124,12 @@ func (s *service) RenameDevice(ctx context.Context, uid models.UID, name, tenant
50124
// It receives a context, used to "control" the request flow and, the namespace name from a models.Namespace and a
51125
// device name from models.Device.
52126
func (s *service) LookupDevice(ctx context.Context, namespace, name string) (*models.Device, error) {
53-
return nil, nil
127+
device, err := s.store.DeviceGet(ctx, store.DeviceIdentName, name)
128+
if err != nil {
129+
return nil, NewErrDeviceLookupNotFound(namespace, name, err)
130+
}
131+
132+
return device, nil
54133
}
55134

56135
func (s *service) OfflineDevice(ctx context.Context, uid models.UID) error {

api/services/sshkeys.go

+30-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ package services
22

33
import (
44
"context"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/x509"
8+
"encoding/pem"
59

610
"github.com/shellhub-io/shellhub/pkg/api/query"
711
"github.com/shellhub-io/shellhub/pkg/api/requests"
812
"github.com/shellhub-io/shellhub/pkg/api/responses"
13+
"github.com/shellhub-io/shellhub/pkg/clock"
914
"github.com/shellhub-io/shellhub/pkg/models"
15+
"golang.org/x/crypto/ssh"
1016
)
1117

1218
type SSHKeysService interface {
@@ -53,5 +59,28 @@ func (s *service) DeletePublicKey(ctx context.Context, fingerprint, tenant strin
5359
}
5460

5561
func (s *service) CreatePrivateKey(ctx context.Context) (*models.PrivateKey, error) {
56-
return nil, nil
62+
key, err := rsa.GenerateKey(rand.Reader, 4096)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
pubKey, err := ssh.NewPublicKey(&key.PublicKey)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
privateKey := &models.PrivateKey{
73+
Data: pem.EncodeToMemory(&pem.Block{
74+
Type: "RSA PRIVATE KEY",
75+
Bytes: x509.MarshalPKCS1PrivateKey(key),
76+
}),
77+
Fingerprint: ssh.FingerprintLegacyMD5(pubKey),
78+
CreatedAt: clock.Now(),
79+
}
80+
81+
if err := s.store.PrivateKeyCreate(ctx, privateKey); err != nil {
82+
return nil, err
83+
}
84+
85+
return privateKey, nil
5786
}

api/services/task.go

+37
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package services
22

33
import (
4+
"bufio"
5+
"bytes"
46
"context"
7+
"slices"
8+
"time"
59

610
"github.com/shellhub-io/shellhub/pkg/worker"
11+
log "github.com/sirupsen/logrus"
712
)
813

914
const (
@@ -14,6 +19,38 @@ const (
1419
// newline-separated list of device UIDs.
1520
func (s *service) DevicesHeartbeat() worker.TaskHandler {
1621
return func(ctx context.Context, payload []byte) error {
22+
log.WithField("task", TaskDevicesHeartbeat.String()).
23+
Info("executing heartbeat task")
24+
25+
scanner := bufio.NewScanner(bytes.NewReader(payload))
26+
scanner.Split(bufio.ScanLines)
27+
28+
uids := make([]string, 0)
29+
for scanner.Scan() {
30+
uid := scanner.Text()
31+
if uid == "" {
32+
continue
33+
}
34+
35+
uids = append(uids, uid)
36+
}
37+
38+
slices.Sort(uids)
39+
uids = slices.Compact(uids)
40+
41+
mCount, err := s.store.DeviceUpdateSeenAt(ctx, uids, time.Now())
42+
if err != nil {
43+
log.WithField("task", TaskDevicesHeartbeat.String()).
44+
WithError(err).
45+
Error("failed to complete the heartbeat task")
46+
47+
return err
48+
}
49+
50+
log.WithField("task", TaskDevicesHeartbeat.String()).
51+
WithField("modified_count", mCount).
52+
Info("finishing heartbeat task")
53+
1754
return nil
1855
}
1956
}

api/store/device.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package store
22

33
import (
44
"context"
5+
"time"
56

67
"github.com/shellhub-io/shellhub/pkg/api/query"
78
"github.com/shellhub-io/shellhub/pkg/models"
@@ -19,11 +20,18 @@ const (
1920
DeviceAcceptableAsFalse
2021
)
2122

23+
type DeviceIdent string
24+
25+
const (
26+
DeviceIdentID DeviceIdent = "id"
27+
DeviceIdentName DeviceIdent = "name"
28+
)
29+
2230
type DeviceStore interface {
2331
DeviceCreate(ctx context.Context, device *models.Device) (insertedID string, err error)
2432

25-
DeviceList(ctx context.Context, status models.DeviceStatus, pagination query.Paginator, filters query.Filters, sorter query.Sorter, acceptable DeviceAcceptable) ([]models.Device, int, error)
26-
DeviceGet(ctx context.Context, uid models.UID) (*models.Device, error)
33+
DeviceList(ctx context.Context, opts ...QueryOption) ([]models.Device, int, error)
34+
DeviceGet(ctx context.Context, ident DeviceIdent, val string) (*models.Device, error)
2735

2836
// DeviceConflicts reports whether the target contains conflicting attributes with the database. Pass zero values for
2937
// attributes you do not wish to match on. For example, the following call checks for conflicts based on email only:
@@ -34,11 +42,7 @@ type DeviceStore interface {
3442
// It returns an array of conflicting attribute fields and an error, if any.
3543
DeviceConflicts(ctx context.Context, target *models.DeviceConflicts) (conflicts []string, has bool, err error)
3644

37-
// DeviceUpdate updates a device with the specified UID that belongs to the specified namespace. It returns [ErrNoDocuments] if none device is found.
38-
DeviceUpdate(ctx context.Context, tenant, uid string, changes *models.DeviceChanges) error
39-
// DeviceBulkdUpdate updates a list of devices. Different than [DeviceStore.DeviceUpdate], it does not differentiate namespaces.
40-
// It returns the number of modified devices and an error if any.
41-
DeviceBulkUpdate(ctx context.Context, uids []string, changes *models.DeviceChanges) (modifiedCount int64, err error)
45+
DeviceUpdateSeenAt(ctx context.Context, ids []string, to time.Time) (updatedCount int64, err error)
4246

4347
DeviceDelete(ctx context.Context, uid models.UID) error
4448
DeviceRename(ctx context.Context, uid models.UID, hostname string) error

0 commit comments

Comments
 (0)