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

charmbracelet / charm / 8113356253

01 Mar 2024 02:57PM UTC coverage: 14.408% (-6.2%) from 20.65%
8113356253

Pull #246

github

web-flow
chore(deps): bump actions/setup-go from 4 to 5

Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #246: chore(deps): bump actions/setup-go from 4 to 5

876 of 6080 relevant lines covered (14.41%)

0.49 hits per line

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

21.47
/server/http.go
1
package server
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "errors"
7
        "fmt"
8
        "io"
9
        "io/fs"
10
        "net/http"
11
        "path/filepath"
12
        "strconv"
13
        "strings"
14
        "time"
15

16
        "github.com/charmbracelet/log"
17

18
        charmfs "github.com/charmbracelet/charm/fs"
19
        charm "github.com/charmbracelet/charm/proto"
20
        "github.com/charmbracelet/charm/server/db"
21
        "github.com/charmbracelet/charm/server/storage"
22
        "github.com/go-jose/go-jose"
23
        "github.com/meowgorithm/babylogger"
24
        "goji.io"
25
        "goji.io/pat"
26
        "goji.io/pattern"
27
        "golang.org/x/sync/errgroup"
28
)
29

30
const resultsPerPage = 50
31

32
// HTTPServer is the HTTP server for the Charm Cloud backend.
33
type HTTPServer struct {
34
        db         db.DB
35
        fstore     storage.FileStore
36
        cfg        *Config
37
        server     *http.Server
38
        health     *http.Server
39
        httpScheme string
40
}
41

42
type providerJSON struct {
43
        Issuer      string   `json:"issuer"`
44
        AuthURL     string   `json:"authorization_endpoint"`
45
        TokenURL    string   `json:"token_endpoint"`
46
        JWKSURL     string   `json:"jwks_uri"`
47
        UserInfoURL string   `json:"userinfo_endpoint"`
48
        Algorithms  []string `json:"id_token_signing_alg_values_supported"`
49
}
50

51
// NewHTTPServer returns a new *HTTPServer with the specified Config.
52
func NewHTTPServer(cfg *Config) (*HTTPServer, error) {
2✔
53
        healthMux := http.NewServeMux()
2✔
54
        // No auth health check endpoint
2✔
55
        healthMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4✔
56
                fmt.Fprintf(w, "We live!")
2✔
57
        }))
2✔
58
        health := &http.Server{
2✔
59
                Addr:              fmt.Sprintf("%s:%d", cfg.BindAddr, cfg.HealthPort),
2✔
60
                Handler:           healthMux,
2✔
61
                ErrorLog:          cfg.errorLog,
2✔
62
                ReadHeaderTimeout: time.Minute,
2✔
63
        }
2✔
64
        mux := goji.NewMux()
2✔
65
        s := &HTTPServer{
2✔
66
                cfg:        cfg,
2✔
67
                health:     health,
2✔
68
                httpScheme: "http",
2✔
69
        }
2✔
70
        s.server = &http.Server{
2✔
71
                Addr:              fmt.Sprintf("%s:%d", s.cfg.BindAddr, s.cfg.HTTPPort),
2✔
72
                Handler:           mux,
2✔
73
                ErrorLog:          s.cfg.errorLog,
2✔
74
                ReadHeaderTimeout: time.Minute,
2✔
75
        }
2✔
76
        if cfg.UseTLS {
2✔
77
                s.httpScheme = "https"
×
78
                s.health.TLSConfig = s.cfg.tlsConfig
×
79
                s.server.TLSConfig = s.cfg.tlsConfig
×
80
        }
×
81

82
        jwtMiddleware, err := JWTMiddleware(
2✔
83
                cfg.jwtKeyPair.JWK.Public(),
2✔
84
                cfg.httpURL().String(),
2✔
85
                []string{"charm"},
2✔
86
        )
2✔
87
        if err != nil {
2✔
88
                return nil, err
×
89
        }
×
90

91
        mux.Use(babylogger.Middleware)
2✔
92
        mux.Use(PublicPrefixesMiddleware([]string{"/v1/public/", "/.well-known/"}))
2✔
93
        mux.Use(jwtMiddleware)
2✔
94
        mux.Use(CharmUserMiddleware(s))
2✔
95
        mux.Use(RequestLimitMiddleware())
2✔
96
        mux.HandleFunc(pat.Get("/v1/id/:id"), s.handleGetUserByID)
2✔
97
        mux.HandleFunc(pat.Get("/v1/bio/:name"), s.handleGetUser)
2✔
98
        mux.HandleFunc(pat.Post("/v1/bio"), s.handlePostUser)
2✔
99
        mux.HandleFunc(pat.Post("/v1/encrypt-key"), s.handlePostEncryptKey)
2✔
100
        mux.HandleFunc(pat.Get("/v1/fs/*"), s.handleGetFile)
2✔
101
        mux.HandleFunc(pat.Post("/v1/fs/*"), s.handlePostFile)
2✔
102
        mux.HandleFunc(pat.Delete("/v1/fs/*"), s.handleDeleteFile)
2✔
103
        mux.HandleFunc(pat.Get("/v1/seq/:name"), s.handleGetSeq)
2✔
104
        mux.HandleFunc(pat.Post("/v1/seq/:name"), s.handlePostSeq)
2✔
105
        mux.HandleFunc(pat.Get("/v1/news"), s.handleGetNewsList)
2✔
106
        mux.HandleFunc(pat.Get("/v1/news/:id"), s.handleGetNews)
2✔
107
        mux.HandleFunc(pat.Get("/v1/public/jwks"), s.handleJWKS)
2✔
108
        mux.HandleFunc(pat.Get("/.well-known/openid-configuration"), s.handleOpenIDConfig)
2✔
109
        s.db = cfg.DB
2✔
110
        s.fstore = cfg.FileStore
2✔
111
        return s, nil
2✔
112
}
113

114
// Start start the HTTP and health servers on the ports specified in the Config.
115
func (s *HTTPServer) Start() error {
2✔
116
        scheme := strings.ToUpper(s.httpScheme)
2✔
117
        errg, _ := errgroup.WithContext(context.Background())
2✔
118
        errg.Go(func() error {
4✔
119
                log.Print("Starting health server", "scheme", scheme, "addr", s.health.Addr)
2✔
120
                if s.cfg.UseTLS {
2✔
121
                        err := s.health.ListenAndServeTLS(s.cfg.TLSCertFile, s.cfg.TLSKeyFile)
×
122
                        if err != http.ErrServerClosed {
×
123
                                return err
×
124
                        }
×
125
                } else {
2✔
126
                        err := s.health.ListenAndServe()
2✔
127
                        if err != http.ErrServerClosed {
2✔
128
                                return err
×
129
                        }
×
130
                }
131
                return nil
2✔
132
        })
133
        errg.Go(func() error {
4✔
134
                log.Print("Starting server", "scheme", scheme, "addr", s.server.Addr)
2✔
135
                if s.cfg.UseTLS {
2✔
136
                        err := s.server.ListenAndServeTLS(s.cfg.TLSCertFile, s.cfg.TLSKeyFile)
×
137
                        if err != http.ErrServerClosed {
×
138
                                return err
×
139
                        }
×
140
                } else {
2✔
141
                        err := s.server.ListenAndServe()
2✔
142
                        if err != http.ErrServerClosed {
2✔
143
                                return err
×
144
                        }
×
145
                }
146
                return nil
2✔
147
        })
148
        return errg.Wait()
2✔
149
}
150

151
// Shutdown gracefully shut down the HTTP and health servers.
152
func (s *HTTPServer) Shutdown(ctx context.Context) error {
×
153
        scheme := strings.ToUpper(s.httpScheme)
×
154
        log.Print("Stopping server", "scheme", scheme, "addr", s.server.Addr)
×
155
        log.Print("Stopping health server", "scheme", scheme, "addr", s.health.Addr)
×
156
        if err := s.health.Shutdown(ctx); err != nil {
×
157
                return err
×
158
        }
×
159
        return s.server.Shutdown(ctx)
×
160
}
161

162
func (s *HTTPServer) renderError(w http.ResponseWriter) {
×
163
        s.renderCustomError(w, "internal error", http.StatusInternalServerError)
×
164
}
×
165

166
func (s *HTTPServer) renderCustomError(w http.ResponseWriter, msg string, status int) {
×
167
        w.Header().Set("Content-Type", "application/json")
×
168
        w.WriteHeader(status)
×
169
        _ = json.NewEncoder(w).Encode(charm.Message{Message: msg})
×
170
}
×
171

172
func (s *HTTPServer) handleJWKS(w http.ResponseWriter, _ *http.Request) {
×
173
        jwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{s.cfg.jwtKeyPair.JWK.Public()}}
×
174
        w.Header().Set("Content-Type", "application/json")
×
175
        w.Header().Set("Access-Control-Allow-Origin", "*")
×
176
        _ = json.NewEncoder(w).Encode(jwks)
×
177
}
×
178

179
func (s *HTTPServer) handleOpenIDConfig(w http.ResponseWriter, _ *http.Request) {
×
180
        pj := providerJSON{JWKSURL: fmt.Sprintf("%s/v1/public/jwks", s.cfg.httpURL())}
×
181
        w.Header().Set("Content-Type", "application/json")
×
182
        w.Header().Set("Access-Control-Allow-Origin", "*")
×
183
        _ = json.NewEncoder(w).Encode(pj)
×
184
}
×
185

186
func (s *HTTPServer) handleGetUserByID(w http.ResponseWriter, r *http.Request) {
×
187
        // nolint: godox
×
188
        // TODO do we need this since you can only get the authed user?
×
189
        u := s.charmUserFromRequest(w, r)
×
190
        w.Header().Set("Content-Type", "application/json")
×
191
        _ = json.NewEncoder(w).Encode(u)
×
192
        s.cfg.Stats.GetUserByID()
×
193
}
×
194

195
func (s *HTTPServer) handleGetUser(w http.ResponseWriter, r *http.Request) {
×
196
        // nolint: godox
×
197
        // TODO do we need this since you can only get the authed user?
×
198
        u := s.charmUserFromRequest(w, r)
×
199
        w.Header().Set("Content-Type", "application/json")
×
200
        _ = json.NewEncoder(w).Encode(u)
×
201
        s.cfg.Stats.GetUser()
×
202
}
×
203

204
func (s *HTTPServer) handlePostUser(w http.ResponseWriter, r *http.Request) {
×
205
        id, err := charmIDFromRequest(r)
×
206
        if err != nil {
×
207
                log.Error("cannot read request body", "err", err)
×
208
                s.renderError(w)
×
209
                return
×
210
        }
×
211
        u := &charm.User{}
×
212
        body, err := io.ReadAll(r.Body)
×
213
        if err != nil {
×
214
                log.Error("cannot read request body", "err", err)
×
215
                s.renderError(w)
×
216
                return
×
217
        }
×
218
        err = json.Unmarshal(body, u)
×
219
        if err != nil {
×
220
                log.Error("cannot decode user json", "err", err)
×
221
                s.renderError(w)
×
222
                return
×
223
        }
×
224
        nu, err := s.db.SetUserName(id, u.Name)
×
225
        if err == charm.ErrNameTaken {
×
226
                s.renderCustomError(w, fmt.Sprintf("username '%s' already taken", u.Name), http.StatusConflict)
×
227
        } else if err != nil {
×
228
                log.Error("cannot set user name", "err", err)
×
229
                s.renderError(w)
×
230
        }
×
231
        w.Header().Set("Content-Type", "application/json")
×
232
        _ = json.NewEncoder(w).Encode(nu)
×
233
        s.cfg.Stats.SetUserName()
×
234
}
235

236
func (s *HTTPServer) handlePostEncryptKey(w http.ResponseWriter, r *http.Request) {
×
237
        u := s.charmUserFromRequest(w, r)
×
238
        ek := &charm.EncryptKey{}
×
239
        body, err := io.ReadAll(r.Body)
×
240
        if err != nil {
×
241
                log.Error("cannot read request body", "err", err)
×
242
                s.renderError(w)
×
243
                return
×
244
        }
×
245
        err = json.Unmarshal(body, ek)
×
246
        if err != nil {
×
247
                log.Error("cannot decode encrypt key json", "err", err)
×
248
                s.renderError(w)
×
249
                return
×
250
        }
×
251
        err = s.db.AddEncryptKeyForPublicKey(u, ek.PublicKey, ek.ID, ek.Key, ek.CreatedAt)
×
252
        if err != nil {
×
253
                log.Error("cannot add encrypt key", "err", err)
×
254
                s.renderError(w)
×
255
                return
×
256
        }
×
257
        s.cfg.Stats.SetUserName()
×
258
}
259

260
func (s *HTTPServer) handleGetSeq(w http.ResponseWriter, r *http.Request) {
×
261
        u := s.charmUserFromRequest(w, r)
×
262
        name := pat.Param(r, "name")
×
263
        seq, err := s.db.GetSeq(u, name)
×
264
        if err != nil {
×
265
                log.Error("cannot get seq", "err", err)
×
266
                s.renderError(w)
×
267
                return
×
268
        }
×
269
        w.Header().Set("Content-Type", "application/json")
×
270
        _ = json.NewEncoder(w).Encode(&charm.SeqMsg{Seq: seq})
×
271
}
272

273
func (s *HTTPServer) handlePostSeq(w http.ResponseWriter, r *http.Request) {
×
274
        u := s.charmUserFromRequest(w, r)
×
275
        name := pat.Param(r, "name")
×
276
        seq, err := s.db.NextSeq(u, name)
×
277
        if err != nil {
×
278
                log.Error("cannot get next seq", "err", err)
×
279
                s.renderError(w)
×
280
                return
×
281
        }
×
282
        w.Header().Set("Content-Type", "application/json")
×
283
        _ = json.NewEncoder(w).Encode(&charm.SeqMsg{Seq: seq})
×
284
}
285

286
func (s *HTTPServer) handlePostFile(w http.ResponseWriter, r *http.Request) {
×
287
        u := s.charmUserFromRequest(w, r)
×
288
        path := filepath.Clean(pattern.Path(r.Context()))
×
289
        ms := r.URL.Query().Get("mode")
×
290
        m, err := strconv.ParseUint(ms, 10, 32)
×
291
        if err != nil {
×
292
                log.Error("file mode not a number", "err", err)
×
293
                s.renderError(w)
×
294
                return
×
295
        }
×
296
        f, fh, err := r.FormFile("data")
×
297
        if err != nil {
×
298
                log.Error("cannot parse form data", "err", err)
×
299
                s.renderError(w)
×
300
                return
×
301
        }
×
302
        defer f.Close() // nolint:errcheck
×
303
        if s.cfg.UserMaxStorage > 0 {
×
304
                stat, err := s.cfg.FileStore.Stat(u.CharmID, "")
×
305
                if err != nil {
×
306
                        log.Error("cannot stat user storage", "err", err)
×
307
                        s.renderError(w)
×
308
                        return
×
309
                }
×
310
                if stat.Size()+fh.Size > s.cfg.UserMaxStorage {
×
311
                        s.renderCustomError(w, "user storage limit exceeded", http.StatusForbidden)
×
312
                        return
×
313
                }
×
314
        }
315
        if err := s.cfg.FileStore.Put(u.CharmID, path, f, fs.FileMode(m)); err != nil {
×
316
                log.Error("cannot post file", "err", err)
×
317
                s.renderError(w)
×
318
                return
×
319
        }
×
320
        s.cfg.Stats.FSFileWritten(u.CharmID, fh.Size)
×
321
}
322

323
func (s *HTTPServer) handleGetFile(w http.ResponseWriter, r *http.Request) {
×
324
        u := s.charmUserFromRequest(w, r)
×
325
        path := filepath.Clean(pattern.Path(r.Context()))
×
326
        f, err := s.cfg.FileStore.Get(u.CharmID, path)
×
327
        if errors.Is(err, fs.ErrNotExist) {
×
328
                s.renderCustomError(w, "file not found", http.StatusNotFound)
×
329
                return
×
330
        }
×
331
        if err != nil {
×
332
                log.Error("cannot get file", "err", err)
×
333
                s.renderError(w)
×
334
                return
×
335
        }
×
336
        defer f.Close() // nolint:errcheck
×
337
        fi, err := f.Stat()
×
338
        if err != nil {
×
339
                log.Error("cannot get file info", "err", err)
×
340
                s.renderError(w)
×
341
                return
×
342
        }
×
343

344
        switch f.(type) {
×
345
        case *charmfs.DirFile:
×
346
                w.Header().Set("Content-Type", "application/json")
×
347
        default:
×
348
                w.Header().Set("Content-Type", "application/octet-stream")
×
349
                w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
×
350
                s.cfg.Stats.FSFileRead(u.CharmID, fi.Size())
×
351
        }
352
        w.Header().Set("X-File-Mode", fmt.Sprintf("%d", fi.Mode()))
×
353
        _, err = io.Copy(w, f)
×
354
        if err != nil {
×
355
                log.Error("cannot copy file", "err", err)
×
356
                s.renderError(w)
×
357
                return
×
358
        }
×
359
}
360

361
func (s *HTTPServer) handleDeleteFile(w http.ResponseWriter, r *http.Request) {
×
362
        u := s.charmUserFromRequest(w, r)
×
363
        path := filepath.Clean(pattern.Path(r.Context()))
×
364
        err := s.cfg.FileStore.Delete(u.CharmID, path)
×
365
        if err != nil {
×
366
                log.Error("cannot delete file", "err", err)
×
367
                s.renderError(w)
×
368
                return
×
369
        }
×
370
}
371

372
func (s *HTTPServer) handleGetNewsList(w http.ResponseWriter, r *http.Request) {
×
373
        w.Header().Set("Content-Type", "application/json")
×
374
        p := r.FormValue("page")
×
375
        if p == "" {
×
376
                p = "1"
×
377
        }
×
378
        page, err := strconv.Atoi(p)
×
379
        if err != nil {
×
380
                log.Error("page not a number", "err", err)
×
381
                w.WriteHeader(http.StatusInternalServerError)
×
382
                return
×
383
        }
×
384

385
        offset := (page - 1) * resultsPerPage
×
386
        tag := r.FormValue("tag")
×
387
        if tag == "" {
×
388
                tag = "server"
×
389
        }
×
390
        ns, err := s.db.GetNewsList(tag, offset)
×
391
        if err != nil {
×
392
                log.Error("cannot get news", "err", err)
×
393
                w.WriteHeader(http.StatusInternalServerError)
×
394
                return
×
395
        }
×
396
        _ = json.NewEncoder(w).Encode(ns)
×
397
        s.cfg.Stats.GetNews()
×
398
}
399

400
func (s *HTTPServer) handleGetNews(w http.ResponseWriter, r *http.Request) {
×
401
        w.Header().Set("Content-Type", "application/json")
×
402
        id := pat.Param(r, "id")
×
403
        news, err := s.db.GetNews(id)
×
404
        if err != nil {
×
405
                log.Error("cannot get news markdown", "err", err)
×
406
                w.WriteHeader(http.StatusInternalServerError)
×
407
                return
×
408
        }
×
409
        _ = json.NewEncoder(w).Encode(news)
×
410
        s.cfg.Stats.GetNews()
×
411
}
412

413
func (s *HTTPServer) charmUserFromRequest(w http.ResponseWriter, r *http.Request) *charm.User {
×
414
        u, ok := r.Context().Value(ctxUserKey).(*charm.User)
×
415
        if !ok {
×
416
                log.Error("could not assign user to request context")
×
417
                s.renderError(w)
×
418
        }
×
419
        return u
×
420
}
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