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

gameap / gameap / 25960901307

16 May 2026 11:29AM UTC coverage: 76.887% (+0.2%) from 76.64%
25960901307

push

github

et-nik
audit logs tests

45395 of 59041 relevant lines covered (76.89%)

33895.16 hits per line

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

63.1
/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, 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, node.GdaemonAPIKey, 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, error) {
5✔
181
        csr, err := h.readCSR(input.GdaemonServerCert)
5✔
182
        if err != nil {
5✔
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✔
188
                return nil, "", errors.WithMessage(err, "failed to sign certificate")
×
189
        }
×
190

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

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

5✔
198
        node.ClientCertificateID, err = h.getClientCertificateID(ctx)
5✔
199
        if err != nil {
5✔
200
                return nil, "", errors.WithMessage(err, "failed to get client certificate ID")
×
201
        }
×
202

203
        now := time.Now()
5✔
204
        node.CreatedAt = &now
5✔
205
        node.UpdatedAt = &now
5✔
206

5✔
207
        if err := h.nodesRepo.Save(ctx, node); err != nil {
5✔
208
                return nil, "", errors.WithMessage(err, "failed to save node")
×
209
        }
×
210

211
        return node, signedCert, nil
5✔
212
}
213

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

227
        if len(certs) > 0 {
5✔
228
                return certs[0].ID, nil
×
229
        }
×
230

231
        certName := xid.New().String()
5✔
232

5✔
233
        certPath := filepath.Join(certificates.ClientCertificatesPath, certName+".crt")
5✔
234
        keyPath := filepath.Join(certificates.ClientCertificatesPath, certName+".key")
5✔
235

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

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

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

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

259
        return clientCertificate.ID, nil
5✔
260
}
261

262
func (h *Handler) readCSR(fileHeaders []*multipart.FileHeader) (string, error) {
5✔
263
        if len(fileHeaders) == 0 {
5✔
264
                return "", ErrGdaemonServerCertRequired
×
265
        }
×
266

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

277
        data, err := io.ReadAll(file)
5✔
278
        if err != nil {
5✔
279
                return "", errors.WithMessage(err, "failed to read certificate file")
×
280
        }
×
281

282
        return string(data), nil
5✔
283
}
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