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

gameap / gameap / 30137012301

24 Jul 2026 11:40PM UTC coverage: 84.416% (-0.3%) from 84.738%
30137012301

Pull #39

github

et-nik
plugins rcon and query
Pull Request #39: Plugins rcon and query

552 of 896 new or added lines in 18 files covered. (61.61%)

7 existing lines in 3 files now uncovered.

49400 of 58520 relevant lines covered (84.42%)

34214.27 hits per line

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

69.71
/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
        "github.com/gameap/gameap/internal/domain"
15
        "github.com/gameap/gameap/internal/filters"
16
        "github.com/gameap/gameap/internal/quercon"
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
        resolver       *quercon.Resolver
29
        responder      base.Responder
30
}
31

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

48
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
11✔
49
        ctx := r.Context()
11✔
50

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

1✔
58
                return
1✔
59
        }
1✔
60

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

3✔
65
                return
3✔
66
        }
3✔
67

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

×
73
                return
×
74
        }
×
75

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

1✔
82
                return
1✔
83
        }
1✔
84

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

3✔
89
                return
3✔
90
        }
3✔
91

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

1✔
96
                return
1✔
97
        }
1✔
98

99
        if server.Rcon == nil || *server.Rcon == "" {
3✔
100
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
101
                        errors.New("rcon password not configured for server"),
1✔
102
                        http.StatusPreconditionFailed,
1✔
103
                ))
1✔
104

1✔
105
                return
1✔
106
        }
1✔
107

108
        output, err := h.executeRconCommand(ctx, server, *game, commandInput.Command)
1✔
109
        if err != nil {
2✔
110
                h.responder.WriteError(ctx, rw, err)
1✔
111

1✔
112
                return
1✔
113
        }
1✔
114

115
        h.responder.Write(ctx, rw, newCommandResponse(output))
×
116
}
117

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

10✔
121
        serverID, err := input.ReadUint("server")
10✔
122
        if err != nil {
11✔
123
                return nil, api.WrapHTTPError(
1✔
124
                        errors.WithMessage(err, "invalid server id"),
1✔
125
                        http.StatusBadRequest,
1✔
126
                )
1✔
127
        }
1✔
128

129
        return h.serverFinder.FindUserServer(ctx, user, serverID)
9✔
130
}
131

132
func (h *Handler) readCommandInput(r *http.Request) (*commandRequest, error) {
6✔
133
        body, err := io.ReadAll(r.Body)
6✔
134
        if err != nil {
6✔
135
                return nil, api.WrapHTTPError(
×
136
                        errors.WithMessage(err, "failed to read request body"),
×
137
                        http.StatusBadRequest,
×
138
                )
×
139
        }
×
140
        defer func() {
12✔
141
                err := r.Body.Close()
6✔
142
                if err != nil {
6✔
143
                        slog.Warn("failed to close request body", "error", err)
×
144
                }
×
145
        }()
146

147
        var commandInput commandRequest
6✔
148
        if err := json.Unmarshal(body, &commandInput); err != nil {
6✔
149
                return nil, api.WrapHTTPError(
×
150
                        errors.WithMessage(err, "failed to parse request body"),
×
151
                        http.StatusBadRequest,
×
152
                )
×
153
        }
×
154

155
        if err := commandInput.Validate(); err != nil {
9✔
156
                return nil, api.WrapHTTPError(
3✔
157
                        err,
3✔
158
                        http.StatusBadRequest,
3✔
159
                )
3✔
160
        }
3✔
161

162
        return &commandInput, nil
3✔
163
}
164

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

180
        return &games[0], nil
2✔
181
}
182

183
func (h *Handler) executeRconCommand(
184
        ctx context.Context,
185
        server *domain.Server,
186
        game domain.Game,
187
        command string,
188
) (string, error) {
1✔
189
        rconAddress := fmt.Sprintf("%s:%d", server.ServerIP, getRconPort(server))
1✔
190

1✔
191
        rconConfig := rcon.Config{
1✔
192
                Address:  rconAddress,
1✔
193
                Password: *server.Rcon,
1✔
194
                Timeout:  10 * time.Second,
1✔
195
        }
1✔
196

1✔
197
        client, err := h.resolver.RconClient(ctx, game, rconConfig)
1✔
198
        if err != nil {
1✔
NEW
199
                if errors.Is(err, quercon.ErrRconProtocolUnsupported) {
×
NEW
200
                        return "", api.WrapHTTPError(
×
NEW
201
                                errors.WithMessage(err, "unsupported game"),
×
NEW
202
                                http.StatusBadRequest,
×
NEW
203
                        )
×
NEW
204
                }
×
205

206
                return "", api.WrapHTTPError(
×
207
                        errors.WithMessage(err, "failed to create rcon client"),
×
208
                        http.StatusInternalServerError,
×
209
                )
×
210
        }
211

212
        if err := client.Open(ctx); err != nil {
2✔
213
                if errors.Is(err, rcon.ErrAuthenticationFailed) {
1✔
214
                        return "", api.WrapHTTPError(
×
215
                                errors.WithMessage(err, "rcon authentication failed"),
×
216
                                http.StatusUnprocessableEntity,
×
217
                        )
×
218
                }
×
219

220
                return "", api.WrapHTTPError(
1✔
221
                        errors.WithMessage(err, "failed to connect to rcon"),
1✔
222
                        http.StatusServiceUnavailable,
1✔
223
                )
1✔
224
        }
225
        defer func(client rcon.Client) {
×
226
                err := client.Close()
×
227
                if err != nil {
×
228
                        slog.Warn(
×
229
                                "failed to close rcon client",
×
230
                                slog.String("error", err.Error()),
×
231
                        )
×
232
                }
×
233
        }(client)
234

235
        output, err := client.Execute(ctx, command)
×
236
        if err != nil {
×
237
                return "", api.WrapHTTPError(
×
238
                        errors.WithMessage(err, "failed to execute rcon command"),
×
239
                        http.StatusInternalServerError,
×
240
                )
×
241
        }
×
242

243
        return output, nil
×
244
}
245

246
func getRconPort(server *domain.Server) int {
3✔
247
        if server.RconPort != nil {
4✔
248
                return *server.RconPort
1✔
249
        }
1✔
250

251
        return server.ServerPort
2✔
252
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc