Skip to content

Commit 86f615f

Browse files
committed
docs(pkg): apply the comment rule to envs, wsconnadapter and jwttoken
A worked sample of what the rule does to code that already exists, so the policy can be judged on a diff rather than on its own description. The godoc the linter asks for gets written — 22 exported symbols across envs and wsconnadapter — and a caller's question that was answered by a comment buried in the body moves up into the contract: why Read hides io.EOF, why a failed ping write tears the adapter down, why Read holds a mutex at all. Everything else goes, and this is where the cost is legible. jwttoken's encoder.go ends with no comments at all: encodeClaims, decodeClaims and evalClaims are unexported, so no linter asks for their documentation and the rule does not write it. The same removes the doc on claimKind, userClaims, deviceClaims, enrollmentDecisionClaims and claimKindFromString, and the note on wsconnadapter's pingInterval and pongTimeout fields saying tests are the only thing that overrides them. jwttoken's TODO list leaves the source as well — a rename nobody is doing today is an issue. The caveat this adapter exists for is gorilla/websocket#441: a websocket connection is message-oriented and the net.Conn it stands in for is not, so reads are stitched across frames and writes are serialised. That is the whole reason for the type, and under this rule the commit message is the only place it can be recorded. It is recorded here. Tests pass and golangci-lint is clean on all three packages with the exported rule on.
1 parent 72f170e commit 86f615f

4 files changed

Lines changed: 64 additions & 32 deletions

File tree

pkg/api/jwttoken/claims.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,21 @@ import (
1212
"github.com/shellhub-io/shellhub/pkg/uuid"
1313
)
1414

15-
// TODO:
16-
// 1. Rename [user|device]Claims.Kind JSON's tag to "kind". (BREAKING CHANGE)
17-
// 2. Rename this package to jwt.
18-
1915
type (
20-
// claimKind represents the type of claims used in JWT tokens.
2116
claimKind string
2217

23-
// userClaims is an auxiliary type that embeds [github.com/golang-jwt/jwt/v5.RegisteredClaims]
24-
// into [github.com/shellhub-io/shellhub/pkg/api/authorizer.UserClaims] to convert it into
25-
// [github.com/golang-jwt/jwt/v5.Claims] for use in an [encode] call.
2618
userClaims struct {
2719
Kind claimKind `json:"claims"`
2820
authorizer.UserClaims
2921
jwt.RegisteredClaims
3022
}
3123

32-
// deviceClaims is an auxiliary type that embeds [github.com/golang-jwt/jwt/v5.RegisteredClaims]
33-
// into [github.com/shellhub-io/shellhub/pkg/api/authorizer.DeviceClaims] to convert it into
34-
// [github.com/golang-jwt/jwt/v5.Claims] for use in an [encode] call.
3524
deviceClaims struct {
3625
Kind claimKind `json:"claims"`
3726
authorizer.DeviceClaims
3827
jwt.RegisteredClaims
3928
}
4029

41-
// enrollmentDecisionClaims is the auxiliary type used to sign a deferred enrollment-decision
42-
// callback token.
4330
enrollmentDecisionClaims struct {
4431
Kind claimKind `json:"claims"`
4532
EnrollmentDecisionClaims
@@ -62,7 +49,6 @@ const (
6249
kindUnknownClaims claimKind = "unknown"
6350
)
6451

65-
// claimKindFromString converts a string to a claimKind.
6652
func claimKindFromString(str string) claimKind {
6753
switch str {
6854
case "user":

pkg/api/jwttoken/encoder.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,16 @@ import (
77
"github.com/golang-jwt/jwt/v5"
88
)
99

10-
// encodeClaims encodes the provided claims into a JWT token using the provided RSA private key.
11-
// It returns the encoded JWT token as a string and any error encountered during the encoding process.
12-
// The claims are signed using the RS256 signing method.
1310
func encodeClaims(claims jwt.Claims, privateKey *rsa.PrivateKey) (string, error) {
1411
return jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(privateKey)
1512
}
1613

17-
// decodeClaims decodes the raw JWT into claims.
1814
func decodeClaims[T jwt.Claims](publicKey *rsa.PublicKey, raw string, claims T) error {
1915
_, err := jwt.ParseWithClaims(raw, claims, evalClaims(publicKey), jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}))
2016

2117
return err
2218
}
2319

24-
// evalClaims evaluates if a token is valid.
2520
func evalClaims(publicKey *rsa.PublicKey) jwt.Keyfunc {
2621
return func(t *jwt.Token) (any, error) {
2722
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {

pkg/envs/envs.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ import (
66
"strings"
77
)
88

9+
// Edition is the ShellHub edition an instance runs as. It decides which features the
10+
// server exposes, so it is read at startup and never per request.
911
type Edition string
1012

1113
const (
12-
Community Edition = "community"
14+
// Community is the open-source edition, and the edition an instance falls back to when
15+
// SHELLHUB_EDITION is unset.
16+
Community Edition = "community"
17+
// Enterprise is the self-hosted paid edition, gated by a licence file.
1318
Enterprise Edition = "enterprise"
14-
Cloud Edition = "cloud"
19+
// Cloud is the hosted edition, which adds billing and the multi-tenant surface on top of
20+
// Enterprise.
21+
Cloud Edition = "cloud"
1522
)
1623

1724
// Backend is an interface for any sort of underlying key/value store.
@@ -58,26 +65,39 @@ func CurrentEdition() Edition {
5865
return edition
5966
}
6067

68+
// IsCommunity reports whether this instance runs the community edition. It panics on an
69+
// unrecognized SHELLHUB_EDITION, as CurrentEdition does.
6170
func IsCommunity() bool {
6271
return CurrentEdition() == Community
6372
}
6473

74+
// IsEnterprise reports whether this instance runs the enterprise edition, which is not the
75+
// same question as "may it use a paid feature" — see IsEnterpriseOrCloud.
6576
func IsEnterprise() bool {
6677
return CurrentEdition() == Enterprise
6778
}
6879

80+
// IsCloud reports whether this instance runs the hosted edition. Billing and namespace
81+
// limits are the features that turn on here and nowhere else.
6982
func IsCloud() bool {
7083
return CurrentEdition() == Cloud
7184
}
7285

86+
// IsEnterpriseOrCloud reports whether the paid feature set is available. This is the check a
87+
// feature gate wants, so that a feature added for cloud stays available to enterprise.
7388
func IsEnterpriseOrCloud() bool {
7489
return CurrentEdition() != Community
7590
}
7691

92+
// IsDevelopment reports whether SHELLHUB_ENV is "development". It gates developer conveniences
93+
// only; never use it to relax a security decision, as the variable is attacker-controlled in
94+
// any deployment that passes the environment through.
7795
func IsDevelopment() bool {
7896
return DefaultBackend.Get("SHELLHUB_ENV") == "development"
7997
}
8098

99+
// ErrParseWithPrefix is joined with the backend's error when ParseWithPrefix fails, so a
100+
// caller can tell a configuration problem from anything else with errors.Is.
81101
var ErrParseWithPrefix = errors.New("failed to parse environment variables for the given prefix")
82102

83103
// ParseWithPrefix parses the environment variables for the a given prefix.
@@ -99,6 +119,8 @@ func ParseWithPrefix[T any](prefix string) (*T, error) {
99119
return envs, nil
100120
}
101121

122+
// ErrParse is joined with the backend's error when Parse fails, so a caller can tell a
123+
// configuration problem from anything else with errors.Is.
102124
var ErrParse = errors.New("failed to parse environment variables")
103125

104126
// Parse parses the environment variables.

pkg/wsconnadapter/wsconnadapter.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@ import (
1313
log "github.com/sirupsen/logrus"
1414
)
1515

16-
// an adapter for representing WebSocket connection as a net.Conn
17-
// some caveats apply: https://github.com/gorilla/websocket/issues/441
18-
16+
// ErrUnexpectedMessageType is returned by Read when the peer sends a frame that is not
17+
// a binary one. The adapter carries a byte stream, so any other type is a protocol error
18+
// rather than something to skip.
1919
var ErrUnexpectedMessageType = errors.New("unexpected websocket message type")
2020

2121
const (
2222
defaultPongTimeout = time.Second * 35
2323
defaultPingInterval = time.Second * 30
2424
)
2525

26+
// Adapter is a net.Conn backed by a websocket connection. The zero value is not usable;
27+
// build one with New. Read and Write are each safe for concurrent use, Close is
28+
// idempotent, and the keep-alive loop starts only once Ping is called.
2629
type Adapter struct {
2730
UUID string
2831
conn *websocket.Conn
@@ -37,20 +40,23 @@ type Adapter struct {
3740
Logger *log.Entry
3841
CreatedAt time.Time
3942

40-
// pingInterval and pongTimeout control the keep-alive loop. They default to
41-
// the package defaults and are only overridden in tests.
4243
pingInterval time.Duration
4344
pongTimeout time.Duration
4445
}
4546

47+
// Option configures an Adapter during New.
4648
type Option func(*Adapter)
4749

50+
// WithID sets the adapter's UUID, which the caller uses to correlate the connection with
51+
// its own bookkeeping. It is not read by the adapter itself.
4852
func WithID(id string) Option {
4953
return func(a *Adapter) {
5054
a.UUID = id
5155
}
5256
}
5357

58+
// WithDevice tags every log line the adapter emits with the tenant and device it belongs
59+
// to, so a connection can be followed through the logs of a busy server.
5460
func WithDevice(tenant string, device string) Option {
5561
return func(a *Adapter) {
5662
a.Logger = a.Logger.WithFields(log.Fields{
@@ -60,6 +66,9 @@ func WithDevice(tenant string, device string) Option {
6066
}
6167
}
6268

69+
// New wraps conn in an Adapter. The adapter takes ownership of conn: closing the adapter
70+
// closes it, and the caller must not use conn directly afterwards. Keep-alive is off until
71+
// Ping is called.
6372
func New(conn *websocket.Conn, options ...Option) *Adapter {
6473
adapter := &Adapter{
6574
conn: conn,
@@ -81,6 +90,14 @@ func New(conn *websocket.Conn, options ...Option) *Adapter {
8190
return adapter
8291
}
8392

93+
// Ping starts the keep-alive loop and returns the channel each pong is announced on. It is
94+
// idempotent: later calls return the same channel without starting a second loop. The
95+
// channel is not buffered and a pong is dropped rather than blocking the read loop, so a
96+
// caller that stops receiving slows nothing down.
97+
//
98+
// A failed ping write is terminal — a broken pipe or a closed socket — so the adapter closes
99+
// itself, which propagates teardown to whoever is reading. Missing pongs for pongTimeout has
100+
// the same effect.
84101
func (a *Adapter) Ping() chan bool {
85102
a.pingOnce.Do(func() {
86103
a.stopPingCh = make(chan struct{})
@@ -113,8 +130,6 @@ func (a *Adapter) Ping() chan bool {
113130
select {
114131
case <-ticker.C:
115132
if err := a.conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(5*time.Second)); err != nil { //nolint:forbidigo // a deadline or an elapsed-time measurement needs the wall clock
116-
// A failed ping write is terminal (broken pipe / closed socket):
117-
// close the adapter so teardown propagates to the consumer, and stop.
118133
a.Logger.
119134
WithError(err).
120135
WithField("lifetime", clock.Now().Sub(a.CreatedAt).String()).
@@ -136,8 +151,13 @@ func (a *Adapter) Ping() chan bool {
136151
return a.pongCh
137152
}
138153

154+
// Read fills b from the current websocket frame and is safe for concurrent use — it holds a
155+
// mutex because it advances the reader the next call resumes from.
156+
//
157+
// A frame boundary is not the end of the stream: the adapter's semantics are a byte stream
158+
// spread over many frames, so an io.EOF from the frame currently being read is reported as a
159+
// nil error and the next call opens the following frame.
139160
func (a *Adapter) Read(b []byte) (int, error) {
140-
// Read() can be called concurrently, and we mutate some internal state here
141161
a.readMutex.Lock()
142162
defer a.readMutex.Unlock()
143163

@@ -158,10 +178,7 @@ func (a *Adapter) Read(b []byte) (int, error) {
158178
if err != nil {
159179
a.reader = nil
160180

161-
// EOF for the current Websocket frame, more will probably come so..
162181
if errors.Is(err, io.EOF) {
163-
// .. we must hide this from the caller since our semantics are a
164-
// stream of bytes across many frames
165182
err = nil
166183
}
167184
}
@@ -173,6 +190,8 @@ func (a *Adapter) Read(b []byte) (int, error) {
173190
return bytesRead, err
174191
}
175192

193+
// Write sends b as a single binary frame and is safe for concurrent use. A short write is
194+
// reported as it happened: the count returned is what the frame took.
176195
func (a *Adapter) Write(b []byte) (int, error) {
177196
a.writeMutex.Lock()
178197
defer a.writeMutex.Unlock()
@@ -194,6 +213,8 @@ func (a *Adapter) Write(b []byte) (int, error) {
194213
return bytesWritten, err
195214
}
196215

216+
// Close stops the keep-alive loop and closes the underlying connection. It is idempotent —
217+
// every call after the first returns the error the first one produced.
197218
func (a *Adapter) Close() error {
198219
a.closeOnce.Do(func() {
199220
if a.stopPingCh != nil {
@@ -207,14 +228,19 @@ func (a *Adapter) Close() error {
207228
return a.closeErr
208229
}
209230

231+
// LocalAddr returns the local address of the underlying websocket connection.
210232
func (a *Adapter) LocalAddr() net.Addr {
211233
return a.conn.LocalAddr()
212234
}
213235

236+
// RemoteAddr returns the peer address of the underlying websocket connection. Behind a proxy
237+
// this is the proxy, not the device.
214238
func (a *Adapter) RemoteAddr() net.Addr {
215239
return a.conn.RemoteAddr()
216240
}
217241

242+
// SetDeadline applies t to both directions, and returns on the first failure — so a failure
243+
// on the read side leaves the write deadline unchanged.
218244
func (a *Adapter) SetDeadline(t time.Time) error {
219245
if err := a.SetReadDeadline(t); err != nil {
220246
a.Logger.WithError(err).Trace("failed to set the deadline")
@@ -225,10 +251,13 @@ func (a *Adapter) SetDeadline(t time.Time) error {
225251
return a.SetWriteDeadline(t)
226252
}
227253

254+
// SetReadDeadline applies t to the read side.
228255
func (a *Adapter) SetReadDeadline(t time.Time) error {
229256
return a.conn.SetReadDeadline(t)
230257
}
231258

259+
// SetWriteDeadline applies t to the write side. It takes the write mutex, so it waits for an
260+
// in-flight Write rather than racing it.
232261
func (a *Adapter) SetWriteDeadline(t time.Time) error {
233262
a.writeMutex.Lock()
234263
defer a.writeMutex.Unlock()

0 commit comments

Comments
 (0)