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

gameap / gameap / 26129195806

19 May 2026 10:29PM UTC coverage: 76.957% (+0.05%) from 76.903%
26129195806

push

github

et-nik
remove legacy

16 of 29 new or added lines in 6 files covered. (55.17%)

574 existing lines in 7 files now uncovered.

42926 of 55779 relevant lines covered (76.96%)

35881.19 hits per line

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

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

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

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

24
const (
25
        typeConsoleHistory = "console.history"
26
        typeConsoleCommand = "console.command"
27
)
28

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

38
type consoleLogService interface {
39
        GetConsoleLog(ctx context.Context, nodeID uint64, serverID uint64, maxBytes int64) (string, error)
40
}
41

42
type Handler struct {
43
        serverFinder      *serversbase.ServerFinder
44
        abilityChecker    *serversbase.AbilityChecker
45
        nodeRepo          repositories.NodeRepository
46
        hub               *ws.Hub
47
        originPatterns    []string
48
        registry          *session.Registry
49
        commandHandler    *handlers.CommandHandler
50
        daemonCommands    daemonCommands
51
        consoleLogService consoleLogService
52
        responder         base.Responder
53
        logger            *slog.Logger
54
}
55

56
func NewHandler(
57
        serverRepo repositories.ServerRepository,
58
        nodeRepo repositories.NodeRepository,
59
        rbac base.RBAC,
60
        hub *ws.Hub,
61
        originPatterns []string,
62
        registry *session.Registry,
63
        commandHandler *handlers.CommandHandler,
64
        daemonCommands daemonCommands,
65
        cls consoleLogService,
66
        responder base.Responder,
67
) *Handler {
8✔
68
        return &Handler{
8✔
69
                serverFinder:      serversbase.NewServerFinder(serverRepo, rbac),
8✔
70
                abilityChecker:    serversbase.NewAbilityChecker(rbac),
8✔
71
                nodeRepo:          nodeRepo,
8✔
72
                hub:               hub,
8✔
73
                originPatterns:    originPatterns,
8✔
74
                registry:          registry,
8✔
75
                commandHandler:    commandHandler,
8✔
76
                daemonCommands:    daemonCommands,
8✔
77
                consoleLogService: cls,
8✔
78
                responder:         responder,
8✔
79
                logger:            slog.Default(),
8✔
80
        }
8✔
81
}
8✔
82

83
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
7✔
84
        ctx := r.Context()
7✔
85

7✔
86
        s := auth.SessionFromContext(ctx)
7✔
87
        if !s.IsAuthenticated() {
8✔
88
                h.responder.WriteError(ctx, rw, api.NewError(http.StatusUnauthorized, "user not authenticated"))
1✔
89

1✔
90
                return
1✔
91
        }
1✔
92

93
        input := api.NewInputReader(r)
6✔
94

6✔
95
        serverID, err := input.ReadUint("server")
6✔
96
        if err != nil {
7✔
97
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
98
                        errors.WithMessage(err, "invalid server id"),
1✔
99
                        http.StatusBadRequest,
1✔
100
                ))
1✔
101

1✔
102
                return
1✔
103
        }
1✔
104

105
        server, err := h.serverFinder.FindUserServer(ctx, s.User, serverID)
5✔
106
        if err != nil {
7✔
107
                h.responder.WriteError(ctx, rw, err)
2✔
108

2✔
109
                return
2✔
110
        }
2✔
111

112
        if err = h.abilityChecker.CheckOrError(
3✔
113
                ctx,
3✔
114
                s.User.ID,
3✔
115
                server.ID,
3✔
116
                []domain.AbilityName{domain.AbilityNameGameServerConsoleView},
3✔
117
        ); err != nil {
4✔
118
                h.responder.WriteError(ctx, rw, err)
1✔
119

1✔
120
                return
1✔
121
        }
1✔
122

123
        node, err := h.findNode(ctx, server.DSID)
2✔
124
        if err != nil {
3✔
125
                h.responder.WriteError(ctx, rw, err)
1✔
126

1✔
127
                return
1✔
128
        }
1✔
129

130
        if !h.registry.IsConnected(uint64(server.DSID)) {
2✔
131
                h.responder.WriteError(ctx, rw, api.NewError(
1✔
132
                        http.StatusServiceUnavailable,
1✔
133
                        "daemon is not connected via grpc",
1✔
134
                ))
1✔
135

1✔
136
                return
1✔
137
        }
1✔
138

UNCOV
139
        conn, err := ws.Accept(rw, r, &websocket.AcceptOptions{
×
UNCOV
140
                OriginPatterns: h.originPatterns,
×
UNCOV
141
        })
×
UNCOV
142
        if err != nil {
×
UNCOV
143
                h.logger.Warn("websocket accept failed", "error", err)
×
UNCOV
144

×
UNCOV
145
                return
×
UNCOV
146
        }
×
147

148
        consoleTopic := ws.ChannelToTopic(channels.BuildRealtimeConsoleOutputChannel(uint64(serverID)))
×
149

×
150
        canSend := h.canSendCommands(ctx, s.User, server)
×
151

×
NEW
152
        h.runGRPCMode(ctx, conn, server, node, consoleTopic, s.User, canSend)
×
153
}
154

155
func (h *Handler) runGRPCMode(
156
        ctx context.Context,
157
        conn *websocket.Conn,
158
        server *domain.Server,
159
        node *domain.Node,
160
        consoleTopic string,
161
        user *domain.User,
162
        canSend bool,
163
) {
×
164
        client := ws.NewClient(ctx, conn, h.hub, nil, h.logger)
×
165
        msgHandler, cleanup := h.newGRPCMessageHandler(ctx, client, server, node, user, canSend)
×
166
        client.SetMessageHandler(msgHandler)
×
167
        defer cleanup()
×
168

×
169
        h.hub.Register(client, consoleTopic)
×
170

×
171
        h.sendConsoleHistory(ctx, client, server, node)
×
172

×
173
        client.Run()
×
174
}
×
175

UNCOV
176
func (h *Handler) sendConsoleHistory(ctx context.Context, client *ws.Client, server *domain.Server, node *domain.Node) {
×
UNCOV
177
        output, err := h.getConsoleLog(ctx, server, node)
×
UNCOV
178
        if err != nil {
×
UNCOV
179
                h.logger.Warn("failed to load console history", "server_id", server.ID, "error", err)
×
UNCOV
180

×
UNCOV
181
                return
×
UNCOV
182
        }
×
183

UNCOV
184
        if output != "" {
×
UNCOV
185
                client.SendMessage(ws.NewOutboundMessage(typeConsoleHistory, consoleHistoryPayload{
×
UNCOV
186
                        Output: output,
×
UNCOV
187
                }))
×
UNCOV
188
        }
×
189
}
190

UNCOV
191
func (h *Handler) getConsoleLog(ctx context.Context, server *domain.Server, node *domain.Node) (string, error) {
×
UNCOV
192
        if h.consoleLogService != nil {
×
UNCOV
193
                output, err := h.consoleLogService.GetConsoleLog(ctx, uint64(node.ID), uint64(server.ID), 0)
×
UNCOV
194
                if err == nil {
×
UNCOV
195
                        return output, nil
×
UNCOV
196
                }
×
197

UNCOV
198
                h.logger.Debug("console log service unavailable, falling back",
×
UNCOV
199
                        "server_id", server.ID, "error", err,
×
UNCOV
200
                )
×
201
        }
202

UNCOV
203
        if node.ScriptGetConsole != nil && *node.ScriptGetConsole != "" {
×
UNCOV
204
                cmd := server.ReplaceServerShortcodes(node, *node.ScriptGetConsole, nil)
×
UNCOV
205

×
UNCOV
206
                result, err := h.daemonCommands.ExecuteCommand(ctx, node, cmd)
×
UNCOV
207
                if err != nil {
×
UNCOV
208
                        return "", errors.WithMessage(err, "failed to execute get console script")
×
UNCOV
209
                }
×
210

UNCOV
211
                return result.Output, nil
×
212
        }
213

NEW
214
        return "", nil
×
215
}
216

217
func (h *Handler) findNode(ctx context.Context, nodeID uint) (*domain.Node, error) {
4✔
218
        nodes, err := h.nodeRepo.Find(ctx, &filters.FindNode{
4✔
219
                IDs: []uint{nodeID},
4✔
220
        }, nil, &filters.Pagination{
4✔
221
                Limit: 1,
4✔
222
        })
4✔
223
        if err != nil {
4✔
224
                return nil, errors.WithMessage(err, "failed to find node")
×
225
        }
×
226

227
        if len(nodes) == 0 {
6✔
228
                return nil, api.NewNotFoundError("node not found")
2✔
229
        }
2✔
230

231
        return &nodes[0], nil
2✔
232
}
233

UNCOV
234
func (h *Handler) canSendCommands(ctx context.Context, user *domain.User, server *domain.Server) bool {
×
UNCOV
235
        err := h.abilityChecker.CheckOrError(
×
UNCOV
236
                ctx,
×
UNCOV
237
                user.ID,
×
UNCOV
238
                server.ID,
×
UNCOV
239
                []domain.AbilityName{domain.AbilityNameGameServerConsoleSend},
×
UNCOV
240
        )
×
UNCOV
241

×
UNCOV
242
        return err == nil
×
UNCOV
243
}
×
244

245
type consoleHistoryPayload struct {
246
        Output string `json:"output"`
247
}
248

249
type consoleCommandPayload struct {
250
        Command string `json:"command"`
251
}
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