• 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

78.82
/internal/api/ws/console/handler.go
1
package console
2

3
import (
4
        "context"
5
        "log/slog"
6
        "net/http"
7
        "os"
8

9
        "github.com/coder/websocket"
10
        "github.com/gameap/gameap/internal/api/base"
11
        serversbase "github.com/gameap/gameap/internal/api/servers/base"
12
        "github.com/gameap/gameap/internal/daemon"
13
        "github.com/gameap/gameap/internal/domain"
14
        "github.com/gameap/gameap/internal/filters"
15
        "github.com/gameap/gameap/internal/grpc/handlers"
16
        "github.com/gameap/gameap/internal/grpc/session"
17
        "github.com/gameap/gameap/internal/pubsub/channels"
18
        "github.com/gameap/gameap/internal/repositories"
19
        "github.com/gameap/gameap/internal/ws"
20
        "github.com/gameap/gameap/pkg/api"
21
        "github.com/gameap/gameap/pkg/auth"
22
        "github.com/pkg/errors"
23
)
24

25
const (
26
        typeConsoleOutput  = "console.output"
27
        typeConsoleHistory = "console.history"
28
        typeConsoleCommand = "console.command"
29
)
30

31
type daemonCommands interface {
32
        ExecuteCommand(
33
                ctx context.Context,
34
                node *domain.Node,
35
                command string,
36
                opts ...daemon.CommandServiceOption,
37
        ) (*daemon.CommandResult, error)
38
}
39

40
type fileService interface {
41
        Download(ctx context.Context, node *domain.Node, filePath string) ([]byte, error)
42
        Upload(
43
                ctx context.Context, node *domain.Node, filePath string,
44
                content []byte, perms os.FileMode, owner daemon.OwnerOptions,
45
        ) error
46
}
47

48
type consoleLogService interface {
49
        GetConsoleLog(ctx context.Context, nodeID uint64, serverID uint64, maxBytes int64) (string, error)
50
}
51

52
type Handler struct {
53
        serverFinder      *serversbase.ServerFinder
54
        abilityChecker    *serversbase.AbilityChecker
55
        nodeRepo          repositories.NodeRepository
56
        hub               *ws.Hub
57
        originPatterns    []string
58
        registry          *session.Registry
59
        commandHandler    *handlers.CommandHandler
60
        daemonCommands    daemonCommands
61
        fileService       fileService
62
        consoleLogService consoleLogService
63
        responder         base.Responder
64
        logger            *slog.Logger
65
}
66

67
func NewHandler(
68
        serverRepo repositories.ServerRepository,
69
        nodeRepo repositories.NodeRepository,
70
        rbac base.RBAC,
71
        hub *ws.Hub,
72
        originPatterns []string,
73
        registry *session.Registry,
74
        commandHandler *handlers.CommandHandler,
75
        daemonCommands daemonCommands,
76
        fileService fileService,
77
        cls consoleLogService,
78
        responder base.Responder,
79
) *Handler {
8✔
80
        return &Handler{
8✔
81
                serverFinder:      serversbase.NewServerFinder(serverRepo, rbac),
8✔
82
                abilityChecker:    serversbase.NewAbilityChecker(rbac),
8✔
83
                nodeRepo:          nodeRepo,
8✔
84
                hub:               hub,
8✔
85
                originPatterns:    originPatterns,
8✔
86
                registry:          registry,
8✔
87
                commandHandler:    commandHandler,
8✔
88
                daemonCommands:    daemonCommands,
8✔
89
                fileService:       fileService,
8✔
90
                consoleLogService: cls,
8✔
91
                responder:         responder,
8✔
92
                logger:            slog.Default(),
8✔
93
        }
8✔
94
}
8✔
95

96
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
7✔
97
        ctx := r.Context()
7✔
98

7✔
99
        s := auth.SessionFromContext(ctx)
7✔
100
        if !s.IsAuthenticated() {
8✔
101
                h.responder.WriteError(ctx, rw, api.NewError(http.StatusUnauthorized, "user not authenticated"))
1✔
102

1✔
103
                return
1✔
104
        }
1✔
105

106
        input := api.NewInputReader(r)
6✔
107

6✔
108
        serverID, err := input.ReadUint("server")
6✔
109
        if err != nil {
7✔
110
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
111
                        errors.WithMessage(err, "invalid server id"),
1✔
112
                        http.StatusBadRequest,
1✔
113
                ))
1✔
114

1✔
115
                return
1✔
116
        }
1✔
117

118
        server, err := h.serverFinder.FindUserServer(ctx, s.User, serverID)
5✔
119
        if err != nil {
7✔
120
                h.responder.WriteError(ctx, rw, err)
2✔
121

2✔
122
                return
2✔
123
        }
2✔
124

125
        if err = h.abilityChecker.CheckOrError(
3✔
126
                ctx,
3✔
127
                s.User.ID,
3✔
128
                server.ID,
3✔
129
                []domain.AbilityName{domain.AbilityNameGameServerConsoleView},
3✔
130
        ); err != nil {
4✔
131
                h.responder.WriteError(ctx, rw, err)
1✔
132

1✔
133
                return
1✔
134
        }
1✔
135

136
        node, err := h.findNode(ctx, server.DSID)
2✔
137
        if err != nil {
3✔
138
                h.responder.WriteError(ctx, rw, err)
1✔
139

1✔
140
                return
1✔
141
        }
1✔
142

143
        conn, err := ws.Accept(rw, r, &websocket.AcceptOptions{
1✔
144
                OriginPatterns: h.originPatterns,
1✔
145
        })
1✔
146
        if err != nil {
2✔
147
                h.logger.Warn("websocket accept failed", "error", err)
1✔
148

1✔
149
                return
1✔
150
        }
1✔
151

152
        consoleTopic := ws.ChannelToTopic(channels.BuildRealtimeConsoleOutputChannel(uint64(serverID)))
×
153

×
154
        canSend := h.canSendCommands(ctx, s.User, server)
×
155

×
156
        if h.registry.IsConnected(uint64(server.DSID)) {
×
157
                h.runGRPCMode(ctx, conn, server, node, consoleTopic, s.User, canSend)
×
158
        } else {
×
159
                h.runLegacyMode(ctx, conn, server, node, consoleTopic, s.User, canSend)
×
160
        }
×
161
}
162

163
func (h *Handler) runGRPCMode(
164
        ctx context.Context,
165
        conn *websocket.Conn,
166
        server *domain.Server,
167
        node *domain.Node,
168
        consoleTopic string,
169
        user *domain.User,
170
        canSend bool,
171
) {
×
172
        client := ws.NewClient(ctx, conn, h.hub, nil, h.logger)
×
173
        msgHandler, cleanup := h.newGRPCMessageHandler(ctx, client, server, node, user, canSend)
×
174
        client.SetMessageHandler(msgHandler)
×
175
        defer cleanup()
×
176

×
177
        h.hub.Register(client, consoleTopic)
×
178

×
179
        h.sendConsoleHistory(ctx, client, server, node)
×
180

×
181
        client.Run()
×
182
}
×
183

184
func (h *Handler) runLegacyMode(
185
        ctx context.Context,
186
        conn *websocket.Conn,
187
        server *domain.Server,
188
        node *domain.Node,
189
        consoleTopic string,
190
        user *domain.User,
191
        canSend bool,
192
) {
×
193
        client := ws.NewClient(ctx, conn, h.hub, nil, h.logger)
×
194
        msgHandler := h.newLegacyMessageHandler(ctx, client, server, node, user, canSend)
×
195
        client.SetMessageHandler(msgHandler)
×
196
        h.hub.Register(client, consoleTopic)
×
197

×
198
        h.sendConsoleHistory(ctx, client, server, node)
×
199

×
200
        poller := newLegacyPoller(client, h.fileService, node, server.Dir, h.logger)
×
201
        go poller.run(ctx)
×
202

×
203
        client.Run()
×
204
}
×
205

206
func (h *Handler) sendConsoleHistory(ctx context.Context, client *ws.Client, server *domain.Server, node *domain.Node) {
3✔
207
        output, err := h.getConsoleLog(ctx, server, node)
3✔
208
        if err != nil {
4✔
209
                h.logger.Warn("failed to load console history", "server_id", server.ID, "error", err)
1✔
210

1✔
211
                return
1✔
212
        }
1✔
213

214
        if output != "" {
3✔
215
                client.SendMessage(ws.NewOutboundMessage(typeConsoleHistory, consoleHistoryPayload{
1✔
216
                        Output: output,
1✔
217
                }))
1✔
218
        }
1✔
219
}
220

221
func (h *Handler) getConsoleLog(ctx context.Context, server *domain.Server, node *domain.Node) (string, error) {
12✔
222
        if h.consoleLogService != nil {
14✔
223
                output, err := h.consoleLogService.GetConsoleLog(ctx, uint64(node.ID), uint64(server.ID), 0)
2✔
224
                if err == nil {
3✔
225
                        return output, nil
1✔
226
                }
1✔
227

228
                h.logger.Debug("console log service unavailable, falling back",
1✔
229
                        "server_id", server.ID, "error", err,
1✔
230
                )
1✔
231
        }
232

233
        if node.ScriptGetConsole != nil && *node.ScriptGetConsole != "" {
15✔
234
                cmd := server.ReplaceServerShortcodes(node, *node.ScriptGetConsole, nil)
4✔
235

4✔
236
                result, err := h.daemonCommands.ExecuteCommand(ctx, node, cmd)
4✔
237
                if err != nil {
5✔
238
                        return "", errors.WithMessage(err, "failed to execute get console script")
1✔
239
                }
1✔
240

241
                return result.Output, nil
3✔
242
        }
243

244
        if h.registry.IsConnected(uint64(node.ID)) {
8✔
245
                return "", nil
1✔
246
        }
1✔
247

248
        return h.downloadOutputFile(ctx, node, server.Dir)
6✔
249
}
250

251
func (h *Handler) downloadOutputFile(ctx context.Context, node *domain.Node, serverDir string) (string, error) {
11✔
252
        outputPath := serverDir + "/output.txt"
11✔
253

11✔
254
        content, err := h.fileService.Download(ctx, node, outputPath)
11✔
255
        if err != nil {
14✔
256
                return "", errors.WithMessage(err, "failed to download console log")
3✔
257
        }
3✔
258

259
        result := string(content)
8✔
260

8✔
261
        const maxSymbols = 65536
8✔
262
        if len(result) > maxSymbols {
9✔
263
                result = result[len(result)-maxSymbols:]
1✔
264
        }
1✔
265

266
        return result, nil
8✔
267
}
268

269
func (h *Handler) findNode(ctx context.Context, nodeID uint) (*domain.Node, error) {
4✔
270
        nodes, err := h.nodeRepo.Find(ctx, &filters.FindNode{
4✔
271
                IDs: []uint{nodeID},
4✔
272
        }, nil, &filters.Pagination{
4✔
273
                Limit: 1,
4✔
274
        })
4✔
275
        if err != nil {
4✔
276
                return nil, errors.WithMessage(err, "failed to find node")
×
277
        }
×
278

279
        if len(nodes) == 0 {
6✔
280
                return nil, api.NewNotFoundError("node not found")
2✔
281
        }
2✔
282

283
        return &nodes[0], nil
2✔
284
}
285

286
func (h *Handler) canSendCommands(ctx context.Context, user *domain.User, server *domain.Server) bool {
2✔
287
        err := h.abilityChecker.CheckOrError(
2✔
288
                ctx,
2✔
289
                user.ID,
2✔
290
                server.ID,
2✔
291
                []domain.AbilityName{domain.AbilityNameGameServerConsoleSend},
2✔
292
        )
2✔
293

2✔
294
        return err == nil
2✔
295
}
2✔
296

297
type consoleHistoryPayload struct {
298
        Output string `json:"output"`
299
}
300

301
type consoleOutputPayload struct {
302
        Chunk string `json:"chunk"`
303
}
304

305
type consoleCommandPayload struct {
306
        Command string `json:"command"`
307
}
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