• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gameap / gameap / 25999900687

17 May 2026 07:02PM UTC coverage: 76.878% (+0.03%) from 76.852%
25999900687

push

github

et-nik
merge main into develop

283 of 349 new or added lines in 42 files covered. (81.09%)

1 existing line in 1 file now uncovered.

46386 of 60337 relevant lines covered (76.88%)

33171.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.87
/internal/api/daemon/createnode/handler.go
1
package createnode
2

3
import (
4
        "context"
5
        "fmt"
6
        "io"
7
        "log/slog"
8
        "mime/multipart"
9
        "net/http"
10
        "path/filepath"
11
        "strconv"
12
        "time"
13

14
        "github.com/gameap/gameap/internal/api/base"
15
        daemonbase "github.com/gameap/gameap/internal/api/daemon/base"
16
        "github.com/gameap/gameap/internal/audit"
17
        "github.com/gameap/gameap/internal/cache"
18
        "github.com/gameap/gameap/internal/certificates"
19
        "github.com/gameap/gameap/internal/domain"
20
        "github.com/gameap/gameap/internal/filters"
21
        "github.com/gameap/gameap/internal/repositories"
22
        "github.com/gameap/gameap/pkg/api"
23
        "github.com/gameap/gameap/pkg/strings"
24
        "github.com/pkg/errors"
25
        "github.com/rs/xid"
26
)
27

28
const (
29
        maxFileSize  = 10 * 1024 * 1024
30
        apiKeyLength = 64
31
)
32

33
var (
34
        ErrInvalidToken               = errors.New("invalid token")
35
        ErrGdaemonServerCertRequired  = errors.New("gdaemon_server_cert is required")
36
        ErrFailedToGetRootCertificate = errors.New("failed to get root certificate")
37
)
38

39
type Handler struct {
40
        cache                 cache.Cache
41
        nodesRepo             repositories.NodeRepository
42
        clientCertificateRepo repositories.ClientCertificateRepository
43
        certificatesSvc       *certificates.Service
44
        responder             base.Responder
45
        audit                 audit.Logger
46
}
47

48
func NewHandler(
49
        cache cache.Cache,
50
        nodesRepo repositories.NodeRepository,
51
        clientCertificateRepo repositories.ClientCertificateRepository,
52
        certificatesSvc *certificates.Service,
53
        responder base.Responder,
54
        auditLogger audit.Logger,
55
) *Handler {
10✔
56
        if auditLogger == nil {
18✔
57
                auditLogger = audit.NopLogger{}
8✔
58
        }
8✔
59

60
        return &Handler{
10✔
61
                cache:                 cache,
10✔
62
                nodesRepo:             nodesRepo,
10✔
63
                clientCertificateRepo: clientCertificateRepo,
10✔
64
                certificatesSvc:       certificatesSvc,
10✔
65
                responder:             responder,
10✔
66
                audit:                 auditLogger,
10✔
67
        }
10✔
68
}
69

70
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
10✔
71
        ctx := r.Context()
10✔
72

10✔
73
        inputReader := api.NewInputReader(r)
10✔
74
        token, err := inputReader.ReadString("token")
10✔
75
        if err != nil {
10✔
76
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
×
77
                        errors.WithMessage(err, "invalid token"),
×
78
                        http.StatusBadRequest,
×
79
                ))
×
80

×
81
                return
×
82
        }
×
83

84
        if err := h.verifyCreateToken(ctx, token); err != nil {
13✔
85
                if errors.Is(err, ErrInvalidToken) || errors.Is(err, cache.ErrNotFound) {
6✔
86
                        h.responder.WriteError(ctx, rw, api.WrapHTTPError(
3✔
87
                                errors.WithMessage(err, "invalid create token"),
3✔
88
                                http.StatusUnauthorized,
3✔
89
                        ))
3✔
90

3✔
91
                        return
3✔
92
                }
3✔
93

94
                h.responder.WriteError(ctx, rw, errors.WithMessage(err, "failed to verify create token"))
×
95

×
96
                return
×
97
        }
98

99
        r.Body = http.MaxBytesReader(rw, r.Body, maxFileSize)
7✔
100
        err = r.ParseMultipartForm(maxFileSize)
7✔
101
        if err != nil {
7✔
102
                slog.ErrorContext(
×
103
                        ctx,
×
104
                        "failed to parse multipart form",
×
105
                        slog.String("error", err.Error()),
×
106
                        slog.String("content_type", r.Header.Get("Content-Type")),
×
107
                        slog.Int64("content_length", r.ContentLength),
×
108
                )
×
109
                h.responder.WriteError(ctx, rw,
×
110
                        api.WrapHTTPError(
×
111
                                errors.WithMessage(err, "failed to parse multipart form"),
×
112
                                http.StatusBadRequest,
×
113
                        ),
×
114
                )
×
115

×
116
                return
×
117
        }
×
118

119
        input := newNodeInputFromRequest(r)
7✔
120

7✔
121
        if err := input.Validate(); err != nil {
9✔
122
                h.responder.WriteError(ctx, rw, errors.WithMessage(err, "validation failed"))
2✔
123

2✔
124
                return
2✔
125
        }
2✔
126

127
        node, apiKey, signedCert, err := h.createNode(ctx, input)
5✔
128
        if err != nil {
5✔
129
                h.responder.WriteError(ctx, rw, errors.WithMessage(err, "failed to create node"))
×
130

×
131
                return
×
132
        }
×
133

134
        audit.SensitiveOp(ctx, h.audit, audit.EventNodeCreate, audit.CategoryNodeOp,
5✔
135
                "node", strconv.FormatUint(uint64(node.ID), 10), "create")
5✔
136

5✔
137
        if err := h.cache.Delete(ctx, daemonbase.AutoCreateTokenCacheKey); err != nil {
5✔
138
                slog.Warn(fmt.Sprintf("failed to delete create token from cache: %v", err))
×
139
        }
×
140

141
        rootCert, err := h.certificatesSvc.Root(ctx)
5✔
142
        if err != nil {
5✔
143
                h.responder.WriteError(ctx, rw, errors.WithMessage(ErrFailedToGetRootCertificate, err.Error()))
×
144

×
145
                return
×
146
        }
×
147

148
        response := buildCreateResponse(node.ID, apiKey, rootCert, signedCert)
5✔
149

5✔
150
        rw.Header().Set("Content-Type", "text/plain")
5✔
151
        _, _ = rw.Write([]byte(response))
5✔
152
}
153

154
func (h *Handler) verifyCreateToken(ctx context.Context, token string) error {
10✔
155
        val, err := h.cache.Get(ctx, daemonbase.AutoCreateTokenCacheKey)
10✔
156
        if err != nil {
11✔
157
                if errors.Is(err, cache.ErrNotFound) {
2✔
158
                        return ErrInvalidToken
1✔
159
                }
1✔
160

161
                return errors.WithMessage(err, "failed to get create token from cache")
×
162
        }
163

164
        if val == nil {
9✔
165
                return ErrInvalidToken
×
166
        }
×
167

168
        storedToken, ok := val.(string)
9✔
169
        if !ok {
9✔
170
                return errors.New("invalid create token type in cache")
×
171
        }
×
172

173
        if token != storedToken {
11✔
174
                return ErrInvalidToken
2✔
175
        }
2✔
176

177
        return nil
7✔
178
}
179

180
func (h *Handler) createNode(ctx context.Context, input *nodeInput) (*domain.Node, string, string, error) {
5✔
181
        csr, err := h.readCSR(input.GdaemonServerCert)
5✔
182
        if err != nil {
5✔
NEW
183
                return nil, "", "", errors.WithMessage(err, "failed to read CSR")
×
184
        }
×
185

186
        signedCert, err := h.certificatesSvc.Sign(ctx, csr, nil)
5✔
187
        if err != nil {
5✔
NEW
188
                return nil, "", "", errors.WithMessage(err, "failed to sign certificate")
×
189
        }
×
190

191
        apiKey, err := strings.CryptoRandomString(apiKeyLength)
5✔
192
        if err != nil {
5✔
NEW
193
                return nil, "", "", errors.WithMessage(err, "failed to generate api key")
×
194
        }
×
195

196
        node := input.ToDomain(apiKey, certificates.RootCACert)
5✔
197

5✔
198
        // Persist only the SHA-256 hash; the plaintext is returned to the daemon
5✔
199
        // once in the response and must never be retrievable from the database.
5✔
200
        node.GdaemonAPIKey = strings.SHA256(apiKey)
5✔
201

5✔
202
        node.ClientCertificateID, err = h.getClientCertificateID(ctx)
5✔
203
        if err != nil {
5✔
NEW
204
                return nil, "", "", errors.WithMessage(err, "failed to get client certificate ID")
×
205
        }
×
206

207
        now := time.Now()
5✔
208
        node.CreatedAt = &now
5✔
209
        node.UpdatedAt = &now
5✔
210

5✔
211
        if err := h.nodesRepo.Save(ctx, node); err != nil {
5✔
NEW
212
                return nil, "", "", errors.WithMessage(err, "failed to save node")
×
213
        }
×
214

215
        return node, apiKey, signedCert, nil
5✔
216
}
217

218
func (h *Handler) getClientCertificateID(ctx context.Context) (uint, error) {
5✔
219
        certs, err := h.clientCertificateRepo.Find(
5✔
220
                ctx,
5✔
221
                nil,
5✔
222
                nil,
5✔
223
                &filters.Pagination{
5✔
224
                        Limit: 1,
5✔
225
                },
5✔
226
        )
5✔
227
        if err != nil {
5✔
228
                return 0, errors.WithMessage(err, "failed to find client certificates")
×
229
        }
×
230

231
        if len(certs) > 0 {
5✔
232
                return certs[0].ID, nil
×
233
        }
×
234

235
        certName := xid.New().String()
5✔
236

5✔
237
        certPath := filepath.Join(certificates.ClientCertificatesPath, certName+".crt")
5✔
238
        keyPath := filepath.Join(certificates.ClientCertificatesPath, certName+".key")
5✔
239

5✔
240
        // Create a new client certificate if none exist
5✔
241
        clientCert, _, err := h.certificatesSvc.Generate(ctx, certPath, keyPath, nil)
5✔
242
        if err != nil {
5✔
243
                return 0, errors.WithMessage(err, "failed to generate client certificate")
×
244
        }
×
245

246
        // Fingerprint the certificate
247
        fingerprint, err := h.certificatesSvc.Fingerprint(clientCert)
5✔
248
        if err != nil {
5✔
249
                return 0, errors.WithMessage(err, "failed to fingerprint client certificate")
×
250
        }
×
251

252
        clientCertificate := domain.ClientCertificate{
5✔
253
                Certificate: certPath,
5✔
254
                PrivateKey:  keyPath,
5✔
255
                Fingerprint: fingerprint,
5✔
256
                Expires:     time.Now().Add(certificates.CertYears * 365 * 24 * time.Hour),
5✔
257
        }
5✔
258

5✔
259
        if err := h.clientCertificateRepo.Save(ctx, &clientCertificate); err != nil {
5✔
260
                return 0, errors.WithMessage(err, "failed to save client certificate")
×
261
        }
×
262

263
        return clientCertificate.ID, nil
5✔
264
}
265

266
func (h *Handler) readCSR(fileHeaders []*multipart.FileHeader) (string, error) {
5✔
267
        if len(fileHeaders) == 0 {
5✔
268
                return "", ErrGdaemonServerCertRequired
×
269
        }
×
270

271
        file, err := fileHeaders[0].Open()
5✔
272
        if err != nil {
5✔
273
                return "", errors.WithMessage(err, "failed to open certificate file")
×
274
        }
×
275
        defer func(f multipart.File) {
10✔
276
                if closeErr := f.Close(); closeErr != nil {
5✔
277
                        slog.Warn(fmt.Sprintf("failed to close certificate file: %v", closeErr))
×
278
                }
×
279
        }(file)
280

281
        data, err := io.ReadAll(file)
5✔
282
        if err != nil {
5✔
283
                return "", errors.WithMessage(err, "failed to read certificate file")
×
284
        }
×
285

286
        return string(data), nil
5✔
287
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc