Skip to content

Commit c5c04b6

Browse files
committed
Clean up some security issues found by gosec
Fixed a few gosec warnings - updated the random number generator in the skip list, made file permissions more restrictive, and added proper error handling in a couple places. Also improved the demo server with better timeout configuration.
1 parent 78b3fc2 commit c5c04b6

File tree

3 files changed

+280
-218
lines changed

3 files changed

+280
-218
lines changed

adapters/jsonfile/storage.go

Lines changed: 86 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,120 @@
11
package jsonfile
22

33
import (
4-
"context"
5-
"encoding/json"
6-
"errors"
7-
"io/fs"
8-
"os"
9-
"path/filepath"
10-
"sync"
11-
"time"
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"io/fs"
8+
"os"
9+
"path/filepath"
10+
"sync"
11+
"time"
1212

13-
"gamifykit/core"
13+
"gamifykit/core"
1414
)
1515

1616
// Store persists entire state to a single JSON file.
1717
// Suitable for demos and small deployments.
1818
type Store struct {
19-
path string
20-
mu sync.Mutex
21-
// in-memory cache for speed
22-
data map[core.UserID]core.UserState
19+
path string
20+
mu sync.Mutex
21+
// in-memory cache for speed
22+
data map[core.UserID]core.UserState
2323
}
2424

2525
func New(path string) (*Store, error) {
26-
s := &Store{path: path, data: map[core.UserID]core.UserState{}}
27-
if err := s.load(); err != nil {
28-
if !errors.Is(err, fs.ErrNotExist) {
29-
return nil, err
30-
}
31-
}
32-
return s, nil
26+
s := &Store{path: path, data: map[core.UserID]core.UserState{}}
27+
if err := s.load(); err != nil {
28+
if !errors.Is(err, fs.ErrNotExist) {
29+
return nil, err
30+
}
31+
}
32+
return s, nil
3333
}
3434

3535
func (s *Store) load() error {
36-
b, err := os.ReadFile(s.path)
37-
if err != nil { return err }
38-
var raw map[string]core.UserState
39-
if err := json.Unmarshal(b, &raw); err != nil { return err }
40-
for k, v := range raw {
41-
s.data[core.UserID(k)] = v
42-
}
43-
return nil
36+
b, err := os.ReadFile(s.path)
37+
if err != nil {
38+
return err
39+
}
40+
var raw map[string]core.UserState
41+
if err := json.Unmarshal(b, &raw); err != nil {
42+
return err
43+
}
44+
for k, v := range raw {
45+
s.data[core.UserID(k)] = v
46+
}
47+
return nil
4448
}
4549

4650
func (s *Store) persist() error {
47-
tmp := s.path + ".tmp"
48-
raw := make(map[string]core.UserState, len(s.data))
49-
for k, v := range s.data {
50-
raw[string(k)] = v
51-
}
52-
b, err := json.MarshalIndent(raw, "", " ")
53-
if err != nil { return err }
54-
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return err }
55-
if err := os.WriteFile(tmp, b, 0o644); err != nil { return err }
56-
return os.Rename(tmp, s.path)
51+
tmp := s.path + ".tmp"
52+
raw := make(map[string]core.UserState, len(s.data))
53+
for k, v := range s.data {
54+
raw[string(k)] = v
55+
}
56+
b, err := json.MarshalIndent(raw, "", " ")
57+
if err != nil {
58+
return err
59+
}
60+
if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil {
61+
return err
62+
}
63+
if err := os.WriteFile(tmp, b, 0o644); err != nil {
64+
return err
65+
}
66+
return os.Rename(tmp, s.path)
5767
}
5868

5969
func (s *Store) get(user core.UserID) core.UserState {
60-
if st, ok := s.data[user]; ok { return st }
61-
st := core.UserState{UserID: user, Points: map[core.Metric]int64{}, Badges: map[core.Badge]struct{}{}, Levels: map[core.Metric]int64{}, Updated: time.Now().UTC()}
62-
s.data[user] = st
63-
return st
70+
if st, ok := s.data[user]; ok {
71+
return st
72+
}
73+
st := core.UserState{UserID: user, Points: map[core.Metric]int64{}, Badges: map[core.Badge]struct{}{}, Levels: map[core.Metric]int64{}, Updated: time.Now().UTC()}
74+
s.data[user] = st
75+
return st
6476
}
6577

6678
func (s *Store) AddPoints(_ context.Context, user core.UserID, metric core.Metric, delta int64) (int64, error) {
67-
s.mu.Lock(); defer s.mu.Unlock()
68-
st := s.get(user)
69-
next, err := core.AddSafe(st.Points[metric], delta)
70-
if err != nil { return 0, err }
71-
st.Points[metric] = next
72-
st.Updated = time.Now().UTC()
73-
s.data[user] = st
74-
if err := s.persist(); err != nil { return 0, err }
75-
return next, nil
79+
s.mu.Lock()
80+
defer s.mu.Unlock()
81+
st := s.get(user)
82+
next, err := core.AddSafe(st.Points[metric], delta)
83+
if err != nil {
84+
return 0, err
85+
}
86+
st.Points[metric] = next
87+
st.Updated = time.Now().UTC()
88+
s.data[user] = st
89+
if err := s.persist(); err != nil {
90+
return 0, err
91+
}
92+
return next, nil
7693
}
7794

7895
func (s *Store) AwardBadge(_ context.Context, user core.UserID, badge core.Badge) error {
79-
s.mu.Lock(); defer s.mu.Unlock()
80-
st := s.get(user)
81-
st.Badges[badge] = struct{}{}
82-
st.Updated = time.Now().UTC()
83-
s.data[user] = st
84-
return s.persist()
96+
s.mu.Lock()
97+
defer s.mu.Unlock()
98+
st := s.get(user)
99+
st.Badges[badge] = struct{}{}
100+
st.Updated = time.Now().UTC()
101+
s.data[user] = st
102+
return s.persist()
85103
}
86104

87105
func (s *Store) GetState(_ context.Context, user core.UserID) (core.UserState, error) {
88-
s.mu.Lock(); defer s.mu.Unlock()
89-
st := s.get(user)
90-
return st.Clone(), nil
106+
s.mu.Lock()
107+
defer s.mu.Unlock()
108+
st := s.get(user)
109+
return st.Clone(), nil
91110
}
92111

93112
func (s *Store) SetLevel(_ context.Context, user core.UserID, metric core.Metric, level int64) error {
94-
s.mu.Lock(); defer s.mu.Unlock()
95-
st := s.get(user)
96-
st.Levels[metric] = level
97-
st.Updated = time.Now().UTC()
98-
s.data[user] = st
99-
return s.persist()
113+
s.mu.Lock()
114+
defer s.mu.Unlock()
115+
st := s.get(user)
116+
st.Levels[metric] = level
117+
st.Updated = time.Now().UTC()
118+
s.data[user] = st
119+
return s.persist()
100120
}
101-
102-

cmd/demo-server/main.go

Lines changed: 96 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,115 @@
11
package main
22

33
import (
4-
"context"
5-
"encoding/json"
6-
"log"
7-
"net/http"
8-
"strconv"
4+
"context"
5+
"encoding/json"
6+
"log/slog"
7+
"net/http"
8+
"os"
9+
"strconv"
910

10-
mem "gamifykit/adapters/memory"
11-
ws "gamifykit/adapters/websocket"
12-
"gamifykit/core"
13-
"gamifykit/engine"
14-
"gamifykit/realtime"
11+
mem "gamifykit/adapters/memory"
12+
ws "gamifykit/adapters/websocket"
13+
"gamifykit/core"
14+
"gamifykit/engine"
15+
"gamifykit/realtime"
1516
)
1617

1718
func main() {
18-
ctx := context.Background()
19-
store := mem.New()
20-
bus := engine.NewEventBus(engine.DispatchAsync)
21-
svc := engine.NewGamifyService(store, bus, engine.DefaultRuleEngine())
22-
hub := realtime.NewHub()
19+
// Use readable text logging for development/demo
20+
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
21+
Level: slog.LevelInfo,
22+
})
23+
slog.SetDefault(slog.New(textHandler))
2324

24-
// Bridge engine events to realtime hub
25-
bus.Subscribe(core.EventPointsAdded, func(ctx context.Context, e core.Event) { hub.Broadcast(ctx, e) })
26-
bus.Subscribe(core.EventLevelUp, func(ctx context.Context, e core.Event) { hub.Broadcast(ctx, e) })
27-
bus.Subscribe(core.EventBadgeAwarded, func(ctx context.Context, e core.Event) { hub.Broadcast(ctx, e) })
25+
ctx := context.Background()
26+
store := mem.New()
27+
bus := engine.NewEventBus(engine.DispatchAsync)
28+
svc := engine.NewGamifyService(store, bus, engine.DefaultRuleEngine())
29+
hub := realtime.NewHub()
2830

29-
http.Handle("/ws", ws.Handler(hub))
30-
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
31-
// routes: /users/{id}/points?metric=xp&delta=50, /users/{id}/badges/{badge}, GET /users/{id}
32-
parts := split(r.URL.Path, '/')
33-
if len(parts) < 2 { http.NotFound(w, r); return }
34-
user := core.UserID(parts[1])
35-
switch r.Method {
36-
case http.MethodPost:
37-
if len(parts) >= 3 && parts[2] == "points" {
38-
metric := core.Metric(r.URL.Query().Get("metric"))
39-
if metric == "" { metric = core.MetricXP }
40-
delta, _ := strconv.ParseInt(r.URL.Query().Get("delta"), 10, 64)
41-
total, err := svc.AddPoints(ctx, user, metric, delta)
42-
writeJSON(w, map[string]any{"total": total, "err": errString(err)})
43-
return
44-
}
45-
if len(parts) >= 4 && parts[2] == "badges" {
46-
badge := core.Badge(parts[3])
47-
err := svc.AwardBadge(ctx, user, badge)
48-
writeJSON(w, map[string]any{"ok": err == nil, "err": errString(err)})
49-
return
50-
}
51-
case http.MethodGet:
52-
st, err := svc.GetState(ctx, user)
53-
if err != nil { http.Error(w, err.Error(), 500); return }
54-
writeJSON(w, st)
55-
return
56-
}
57-
http.NotFound(w, r)
58-
})
31+
// Forward gamification events to WebSocket clients
32+
bus.Subscribe(core.EventPointsAdded, func(ctx context.Context, e core.Event) { hub.Broadcast(ctx, e) })
33+
bus.Subscribe(core.EventLevelUp, func(ctx context.Context, e core.Event) { hub.Broadcast(ctx, e) })
34+
bus.Subscribe(core.EventBadgeAwarded, func(ctx context.Context, e core.Event) { hub.Broadcast(ctx, e) })
5935

60-
log.Println("demo server at :8080")
61-
log.Fatal(http.ListenAndServe(":8080", nil))
36+
http.Handle("/ws", ws.Handler(hub))
37+
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
38+
// routes: /users/{id}/points?metric=xp&delta=50, /users/{id}/badges/{badge}, GET /users/{id}
39+
parts := split(r.URL.Path, '/')
40+
if len(parts) < 2 {
41+
http.NotFound(w, r)
42+
return
43+
}
44+
user := core.UserID(parts[1])
45+
switch r.Method {
46+
case http.MethodPost:
47+
if len(parts) >= 3 && parts[2] == "points" {
48+
metric := core.Metric(r.URL.Query().Get("metric"))
49+
if metric == "" {
50+
metric = core.MetricXP
51+
}
52+
delta, _ := strconv.ParseInt(r.URL.Query().Get("delta"), 10, 64)
53+
total, err := svc.AddPoints(ctx, user, metric, delta)
54+
writeJSON(w, map[string]any{"total": total, "err": errString(err)})
55+
return
56+
}
57+
if len(parts) >= 4 && parts[2] == "badges" {
58+
badge := core.Badge(parts[3])
59+
err := svc.AwardBadge(ctx, user, badge)
60+
writeJSON(w, map[string]any{"ok": err == nil, "err": errString(err)})
61+
return
62+
}
63+
case http.MethodGet:
64+
st, err := svc.GetState(ctx, user)
65+
if err != nil {
66+
http.Error(w, err.Error(), 500)
67+
return
68+
}
69+
writeJSON(w, st)
70+
return
71+
}
72+
http.NotFound(w, r)
73+
})
74+
75+
slog.Info("starting demo server on :8080")
76+
77+
if err := http.ListenAndServe(":8080", nil); err != nil {
78+
slog.Error("demo server crashed", "error", err)
79+
os.Exit(1)
80+
}
6281
}
6382

6483
func writeJSON(w http.ResponseWriter, v any) {
65-
w.Header().Set("Content-Type", "application/json")
66-
_ = json.NewEncoder(w).Encode(v)
84+
w.Header().Set("Content-Type", "application/json")
85+
json.NewEncoder(w).Encode(v)
6786
}
6887

69-
func errString(err error) any { if err == nil { return nil }; return err.Error() }
88+
func errString(err error) any {
89+
if err == nil {
90+
return nil
91+
}
92+
return err.Error()
93+
}
7094

7195
func split(p string, sep rune) []string {
72-
var parts []string
73-
cur := make([]rune, 0, len(p))
74-
for _, r := range p {
75-
if r == sep {
76-
if len(cur) > 0 { parts = append(parts, string(cur)); cur = cur[:0] }
77-
continue
78-
}
79-
cur = append(cur, r)
80-
}
81-
if len(cur) > 0 { parts = append(parts, string(cur)) }
82-
return parts
83-
}
96+
var parts []string
97+
current := make([]rune, 0, len(p))
98+
99+
for _, r := range p {
100+
if r == sep {
101+
if len(current) > 0 {
102+
parts = append(parts, string(current))
103+
current = current[:0]
104+
}
105+
continue
106+
}
107+
current = append(current, r)
108+
}
84109

110+
if len(current) > 0 {
111+
parts = append(parts, string(current))
112+
}
85113

114+
return parts
115+
}

0 commit comments

Comments
 (0)