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

gameap / gameap / 19519990805

19 Nov 2025 11:38PM UTC coverage: 61.004% (+0.03%) from 60.974%
19519990805

push

github

et-nik
ability checking fixes

353 of 512 new or added lines in 30 files covered. (68.95%)

4 existing lines in 2 files now uncovered.

24176 of 39630 relevant lines covered (61.0%)

330.88 hits per line

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

73.81
/internal/api/servers/rcon/postcommand/handler.go
1
package postcommand
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "io"
8
        "log/slog"
9
        "net/http"
10
        "time"
11

12
        "github.com/gameap/gameap/internal/api/base"
13
        serversbase "github.com/gameap/gameap/internal/api/servers/base"
14
        rconbase "github.com/gameap/gameap/internal/api/servers/rcon/base"
15
        "github.com/gameap/gameap/internal/domain"
16
        "github.com/gameap/gameap/internal/filters"
17
        "github.com/gameap/gameap/internal/repositories"
18
        "github.com/gameap/gameap/pkg/api"
19
        "github.com/gameap/gameap/pkg/auth"
20
        "github.com/gameap/gameap/pkg/quercon/rcon"
21
        "github.com/pkg/errors"
22
)
23

24
type Handler struct {
25
        serverFinder   *serversbase.ServerFinder
26
        abilityChecker *serversbase.AbilityChecker
27
        gameRepo       repositories.GameRepository
28
        responder      base.Responder
29
}
30

31
func NewHandler(
32
        serverRepo repositories.ServerRepository,
33
        gameRepo repositories.GameRepository,
34
        rbac base.RBAC,
35
        responder base.Responder,
36
) *Handler {
12✔
37
        return &Handler{
12✔
38
                serverFinder:   serversbase.NewServerFinder(serverRepo, rbac),
12✔
39
                abilityChecker: serversbase.NewAbilityChecker(rbac),
12✔
40
                gameRepo:       gameRepo,
12✔
41
                responder:      responder,
12✔
42
        }
12✔
43
}
12✔
44

45
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
11✔
46
        ctx := r.Context()
11✔
47

11✔
48
        session := auth.SessionFromContext(ctx)
11✔
49
        if !session.IsAuthenticated() {
12✔
50
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
51
                        errors.New("user not authenticated"),
1✔
52
                        http.StatusUnauthorized,
1✔
53
                ))
1✔
54

1✔
55
                return
1✔
56
        }
1✔
57

58
        server, err := h.getServer(ctx, r, session.User)
10✔
59
        if err != nil {
13✔
60
                h.responder.WriteError(ctx, rw, err)
3✔
61

3✔
62
                return
3✔
63
        }
3✔
64

65
        if err = h.abilityChecker.CheckOrError(
7✔
66
                ctx, session.User.ID, server.ID, []domain.AbilityName{domain.AbilityNameGameServerRconConsole},
7✔
67
        ); err != nil {
7✔
NEW
68
                h.responder.WriteError(ctx, rw, err)
×
NEW
69

×
NEW
70
                return
×
NEW
71
        }
×
72

73
        if !server.IsOnline() {
8✔
74
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
75
                        errors.New("server is offline"),
1✔
76
                        http.StatusServiceUnavailable,
1✔
77
                ))
1✔
78

1✔
79
                return
1✔
80
        }
1✔
81

82
        commandInput, err := h.readCommandInput(r)
6✔
83
        if err != nil {
9✔
84
                h.responder.WriteError(ctx, rw, err)
3✔
85

3✔
86
                return
3✔
87
        }
3✔
88

89
        game, err := h.findGame(ctx, server.GameID)
3✔
90
        if err != nil {
4✔
91
                h.responder.WriteError(ctx, rw, err)
1✔
92

1✔
93
                return
1✔
94
        }
1✔
95

96
        protocol, err := rconbase.DetermineProtocolByEngine(game.Engine)
2✔
97
        if err != nil {
2✔
98
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
×
99
                        errors.WithMessage(err, "unsupported game engine"),
×
100
                        http.StatusBadRequest,
×
101
                ))
×
102

×
103
                return
×
104
        }
×
105

106
        if server.Rcon == nil || *server.Rcon == "" {
3✔
107
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
108
                        errors.New("rcon password not configured for server"),
1✔
109
                        http.StatusPreconditionFailed,
1✔
110
                ))
1✔
111

1✔
112
                return
1✔
113
        }
1✔
114

115
        output, err := h.executeRconCommand(ctx, server, protocol, commandInput.Command)
1✔
116
        if err != nil {
2✔
117
                h.responder.WriteError(ctx, rw, err)
1✔
118

1✔
119
                return
1✔
120
        }
1✔
121

122
        h.responder.Write(ctx, rw, newCommandResponse(output))
×
123
}
124

125
func (h *Handler) getServer(ctx context.Context, r *http.Request, user *domain.User) (*domain.Server, error) {
10✔
126
        input := api.NewInputReader(r)
10✔
127

10✔
128
        serverID, err := input.ReadUint("server")
10✔
129
        if err != nil {
11✔
130
                return nil, api.WrapHTTPError(
1✔
131
                        errors.WithMessage(err, "invalid server id"),
1✔
132
                        http.StatusBadRequest,
1✔
133
                )
1✔
134
        }
1✔
135

136
        return h.serverFinder.FindUserServer(ctx, user, serverID)
9✔
137
}
138

139
func (h *Handler) readCommandInput(r *http.Request) (*commandRequest, error) {
6✔
140
        body, err := io.ReadAll(r.Body)
6✔
141
        if err != nil {
6✔
142
                return nil, api.WrapHTTPError(
×
143
                        errors.WithMessage(err, "failed to read request body"),
×
144
                        http.StatusBadRequest,
×
145
                )
×
146
        }
×
147
        defer func() {
12✔
148
                err := r.Body.Close()
6✔
149
                if err != nil {
6✔
150
                        slog.Warn("failed to close request body", "error", err)
×
151
                }
×
152
        }()
153

154
        var commandInput commandRequest
6✔
155
        if err := json.Unmarshal(body, &commandInput); err != nil {
6✔
156
                return nil, api.WrapHTTPError(
×
157
                        errors.WithMessage(err, "failed to parse request body"),
×
158
                        http.StatusBadRequest,
×
159
                )
×
160
        }
×
161

162
        if err := commandInput.Validate(); err != nil {
9✔
163
                return nil, api.WrapHTTPError(
3✔
164
                        err,
3✔
165
                        http.StatusBadRequest,
3✔
166
                )
3✔
167
        }
3✔
168

169
        return &commandInput, nil
3✔
170
}
171

172
func (h *Handler) findGame(ctx context.Context, gameID string) (*domain.Game, error) {
3✔
173
        games, err := h.gameRepo.Find(ctx, filters.FindGameByCodes(gameID), nil, nil)
3✔
174
        if err != nil {
3✔
175
                return nil, api.WrapHTTPError(
×
176
                        errors.WithMessage(err, "failed to find game for server"),
×
177
                        http.StatusInternalServerError,
×
178
                )
×
179
        }
×
180
        if len(games) == 0 {
4✔
181
                return nil, api.WrapHTTPError(
1✔
182
                        errors.New("game for server not found"),
1✔
183
                        http.StatusInternalServerError,
1✔
184
                )
1✔
185
        }
1✔
186

187
        return &games[0], nil
2✔
188
}
189

190
func (h *Handler) executeRconCommand(
191
        ctx context.Context,
192
        server *domain.Server,
193
        protocol rcon.Protocol,
194
        command string,
195
) (string, error) {
1✔
196
        rconAddress := fmt.Sprintf("%s:%d", server.ServerIP, getRconPort(server))
1✔
197

1✔
198
        rconConfig := rcon.Config{
1✔
199
                Address:  rconAddress,
1✔
200
                Password: *server.Rcon,
1✔
201
                Protocol: protocol,
1✔
202
                Timeout:  10 * time.Second,
1✔
203
        }
1✔
204

1✔
205
        client, err := rcon.NewClient(rconConfig)
1✔
206
        if err != nil {
1✔
207
                return "", api.WrapHTTPError(
×
208
                        errors.WithMessage(err, "failed to create rcon client"),
×
209
                        http.StatusInternalServerError,
×
210
                )
×
211
        }
×
212

213
        if err := client.Open(ctx); err != nil {
2✔
214
                return "", api.WrapHTTPError(
1✔
215
                        errors.WithMessage(err, "failed to connect to rcon"),
1✔
216
                        http.StatusServiceUnavailable,
1✔
217
                )
1✔
218
        }
1✔
219
        defer client.Close()
×
220

×
221
        output, err := client.Execute(ctx, command)
×
222
        if err != nil {
×
223
                return "", api.WrapHTTPError(
×
224
                        errors.WithMessage(err, "failed to execute rcon command"),
×
225
                        http.StatusInternalServerError,
×
226
                )
×
227
        }
×
228

229
        return output, nil
×
230
}
231

232
func getRconPort(server *domain.Server) int {
3✔
233
        if server.RconPort != nil {
4✔
234
                return *server.RconPort
1✔
235
        }
1✔
236

237
        return server.ServerPort
2✔
238
}
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