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

oxyno-zeta / s3-proxy / 5553443028

14 Jul 2023 11:07AM UTC coverage: 86.174% (-0.02%) from 86.195%
5553443028

push

github

oxyno-zeta
feat: Upgrade dependencies

4949 of 5743 relevant lines covered (86.17%)

39.22 hits per line

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

82.89
/pkg/s3-proxy/server/server.go
1
package server
2

3
import (
4
        "net/http"
5
        "net/url"
6
        "strconv"
7
        "time"
8

9
        "emperror.dev/errors"
10
        "github.com/go-chi/chi/v5"
11
        "github.com/go-chi/chi/v5/middleware"
12
        "github.com/go-chi/cors"
13
        "github.com/go-chi/httptracer"
14
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/authx/authentication"
15
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/authx/authorization"
16
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/bucket"
17
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/config"
18
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/log"
19
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/metrics"
20
        responsehandler "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/response-handler"
21
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/s3client"
22
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/server/middlewares"
23
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/tracing"
24
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/version"
25
        "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/webhook"
26
        "github.com/thoas/go-funk"
27
)
28

29
type Server struct {
30
        logger          log.Logger
31
        cfgManager      config.Manager
32
        metricsCl       metrics.Client
33
        server          *http.Server
34
        tracingSvc      tracing.Service
35
        s3clientManager s3client.Manager
36
        webhookManager  webhook.Manager
37
}
38

39
func NewServer(
40
        logger log.Logger,
41
        cfgManager config.Manager,
42
        metricsCl metrics.Client,
43
        tracingSvc tracing.Service,
44
        s3clientManager s3client.Manager,
45
        webhookManager webhook.Manager,
46
) *Server {
12✔
47
        return &Server{
12✔
48
                logger:          logger,
12✔
49
                cfgManager:      cfgManager,
12✔
50
                metricsCl:       metricsCl,
12✔
51
                tracingSvc:      tracingSvc,
12✔
52
                s3clientManager: s3clientManager,
12✔
53
                webhookManager:  webhookManager,
12✔
54
        }
12✔
55
}
12✔
56

57
func (svr *Server) Listen() error {
19✔
58
        svr.logger.Infof("Server listening on %s", svr.server.Addr)
19✔
59

19✔
60
        var err error
19✔
61

19✔
62
        // Listen (either HTTPS or HTTP)
19✔
63
        if svr.server.TLSConfig != nil {
27✔
64
                err = svr.server.ListenAndServeTLS("", "")
8✔
65
        } else {
19✔
66
                err = svr.server.ListenAndServe()
11✔
67
        }
11✔
68

69
        // Check error
70
        if err != nil {
38✔
71
                return errors.WithStack(err)
19✔
72
        }
19✔
73

74
        // Default
75
        return nil
×
76
}
77

78
func (svr *Server) GenerateServer() error {
32✔
79
        // Get configuration
32✔
80
        cfg := svr.cfgManager.GetConfig()
32✔
81

32✔
82
        // Generate router
32✔
83
        r, err := svr.generateRouter()
32✔
84
        if err != nil {
33✔
85
                return err
1✔
86
        }
1✔
87

88
        // Create server
89
        addr := cfg.Server.ListenAddr + ":" + strconv.Itoa(cfg.Server.Port)
31✔
90
        server := &http.Server{ //nolint: gosec // Set after
31✔
91
                Addr:    addr,
31✔
92
                Handler: r,
31✔
93
        }
31✔
94

31✔
95
        // Inject timeouts
31✔
96
        err = injectServerTimeout(server, cfg.Server.Timeouts)
31✔
97
        // Check error
31✔
98
        if err != nil {
31✔
99
                return err
×
100
        }
×
101

102
        // Get the TLS configuration (if necessary).
103
        tlsConfig, err := generateTLSConfig(cfg.Server.SSL, svr.logger)
31✔
104
        if err != nil {
43✔
105
                return errors.Wrap(err, "failed to create TLS configuration for server")
12✔
106
        }
12✔
107

108
        server.TLSConfig = tlsConfig
19✔
109

19✔
110
        // Prepare for configuration onChange
19✔
111
        svr.cfgManager.AddOnChangeHook(func() {
19✔
112
                // Generate router
×
113
                r, err2 := svr.generateRouter()
×
114
                if err2 != nil {
×
115
                        svr.logger.Fatal(err2)
×
116
                }
×
117
                // Change server handler
118
                server.Handler = r
×
119
                svr.logger.Info("Server handler reloaded")
×
120
        })
121

122
        // Store server
123
        svr.server = server
19✔
124

19✔
125
        return nil
19✔
126
}
127

128
func (svr *Server) generateRouter() (http.Handler, error) {
108✔
129
        // Get configuration
108✔
130
        cfg := svr.cfgManager.GetConfig()
108✔
131

108✔
132
        // Create authentication service
108✔
133
        authenticationSvc := authentication.NewAuthenticationService(cfg, svr.cfgManager, svr.metricsCl)
108✔
134

108✔
135
        // Create router
108✔
136
        r := chi.NewRouter()
108✔
137

108✔
138
        // Check if we need to enabled the compress middleware
108✔
139
        if cfg.Server.Compress != nil && *cfg.Server.Compress.Enabled {
195✔
140
                r.Use(middleware.Compress(
87✔
141
                        cfg.Server.Compress.Level,
87✔
142
                        cfg.Server.Compress.Types...,
87✔
143
                ))
87✔
144
        }
87✔
145

146
        // Check if no cache is enabled or not
147
        if cfg.Server.Cache == nil || cfg.Server.Cache.NoCacheEnabled {
215✔
148
                // Apply no cache
107✔
149
                r.Use(middleware.NoCache)
107✔
150
        } else {
108✔
151
                // Apply S3 proxy cache management middleware
1✔
152
                r.Use(middlewares.CacheManagement(cfg.Server.Cache))
1✔
153
        }
1✔
154

155
        r.Use(middleware.RequestID)
108✔
156
        r.Use(middleware.RealIP)
108✔
157
        // Manage tracing
108✔
158
        // Create http tracer configuration
108✔
159
        httptraCfg := httptracer.Config{
108✔
160
                ServiceName:    "s3-proxy",
108✔
161
                ServiceVersion: version.GetVersion().Version,
108✔
162
                SampleRate:     1,
108✔
163
                OperationName:  "http.request",
108✔
164
                Tags:           cfg.Tracing.FixedTags,
108✔
165
        }
108✔
166
        // Put tracing middlewares
108✔
167
        r.Use(httptracer.Tracer(svr.tracingSvc.GetTracer(), httptraCfg))
108✔
168
        r.Use(middlewares.ImproveTracing())
108✔
169
        r.Use(log.NewStructuredLogger(
108✔
170
                svr.logger,
108✔
171
                tracing.GetTraceIDFromRequest,
108✔
172
        ))
108✔
173
        r.Use(log.HTTPAddLoggerToContextMiddleware())
108✔
174
        r.Use(svr.metricsCl.Instrument("business"))
108✔
175
        // Recover panic
108✔
176
        r.Use(middleware.Recoverer)
108✔
177

108✔
178
        // Check if cors is enabled
108✔
179
        if cfg.Server != nil && cfg.Server.CORS != nil && cfg.Server.CORS.Enabled {
114✔
180
                // Generate CORS
6✔
181
                cc := generateCors(cfg.Server, svr.logger.GetCorsLogger())
6✔
182
                // Apply CORS handler
6✔
183
                r.Use(cc.Handler)
6✔
184
        }
6✔
185

186
        // Check if auth if enabled and oidc enabled
187
        if cfg.AuthProviders != nil && cfg.AuthProviders.OIDC != nil {
119✔
188
                for k, v := range cfg.AuthProviders.OIDC {
23✔
189
                        // Add oidc endpoints
12✔
190
                        err := authenticationSvc.OIDCEndpoints(k, v, r)
12✔
191
                        // Check error
12✔
192
                        if err != nil {
13✔
193
                                return nil, err
1✔
194
                        }
1✔
195
                }
196
        }
197

198
        notFoundHandler := func(w http.ResponseWriter, r *http.Request) {
109✔
199
                // Answer with general not found handler
2✔
200
                responsehandler.GeneralNotFoundError(r, w, svr.cfgManager)
2✔
201
        }
2✔
202

203
        internalServerHandlerGen := func(err error) http.HandlerFunc {
107✔
204
                return func(w http.ResponseWriter, r *http.Request) {
×
205
                        // Answer with general internal server error handler
×
206
                        responsehandler.GeneralInternalServerError(
×
207
                                r,
×
208
                                w,
×
209
                                svr.cfgManager,
×
210
                                errors.WithStack(err),
×
211
                        )
×
212
                }
×
213
        }
214

215
        // Create host router
216
        hr := NewHostRouter(notFoundHandler, internalServerHandlerGen)
107✔
217

107✔
218
        // Load main route only if main bucket path support option isn't enabled
107✔
219
        if cfg.ListTargets.Enabled {
123✔
220
                // Create new router
16✔
221
                rt := chi.NewRouter()
16✔
222
                // Add middleware in order to add response handler
16✔
223
                rt.Use(responsehandler.HTTPMiddleware(svr.cfgManager, ""))
16✔
224
                // Make list of resources from resource
16✔
225
                resources := make([]*config.Resource, 0)
16✔
226
                if cfg.ListTargets.Resource != nil {
30✔
227
                        resources = append(resources, cfg.ListTargets.Resource)
14✔
228
                }
14✔
229
                // Manage path for list targets feature
230
                // Loop over path list
231
                funk.ForEach(cfg.ListTargets.Mount.Path, func(path string) {
32✔
232
                        rt.Route(path, func(rt2 chi.Router) {
32✔
233
                                // Add authentication middleware to router
16✔
234
                                rt2 = rt2.With(authenticationSvc.Middleware(resources))
16✔
235

16✔
236
                                // Add authorization middleware to router
16✔
237
                                rt2 = rt2.With(authorization.Middleware(svr.cfgManager, svr.metricsCl))
16✔
238

16✔
239
                                rt2.Get("/", func(rw http.ResponseWriter, req *http.Request) {
23✔
240
                                        // Get response handler
7✔
241
                                        resHan := responsehandler.GetResponseHandlerFromContext(req.Context())
7✔
242

7✔
243
                                        // Handle target list
7✔
244
                                        resHan.TargetList()
7✔
245
                                })
7✔
246
                        })
247
                })
248
                // Create domain
249
                domain := cfg.ListTargets.Mount.Host
16✔
250
                if domain == "" {
32✔
251
                        domain = "*"
16✔
252
                }
16✔
253
                // Mount domain from configuration
254
                hr.Map(domain, rt)
16✔
255
        }
256

257
        // Load all targets routes
258
        for targetKey, tgt := range cfg.Targets {
253✔
259
                // Manage domain
146✔
260
                domain := tgt.Mount.Host
146✔
261
                if domain == "" {
290✔
262
                        domain = "*"
144✔
263
                }
144✔
264
                // Get router from hostrouter if exists
265
                rt := hr.Get(domain)
146✔
266
                // Check nil
146✔
267
                if rt == nil {
236✔
268
                        // Create a new router
90✔
269
                        rt = chi.NewRouter()
90✔
270
                }
90✔
271
                // Loop over path list
272
                funk.ForEach(tgt.Mount.Path, func(path string) {
292✔
273
                        rt.Route(path, func(rt2 chi.Router) {
292✔
274
                                // Add middleware in order to add response handler
146✔
275
                                rt2.Use(responsehandler.HTTPMiddleware(svr.cfgManager, targetKey))
146✔
276

146✔
277
                                // Add Bucket request context middleware to initialize it
146✔
278
                                rt2.Use(bucket.HTTPMiddleware(tgt, path, svr.s3clientManager, svr.webhookManager))
146✔
279

146✔
280
                                // Add authentication middleware to router
146✔
281
                                rt2.Use(authenticationSvc.Middleware(tgt.Resources))
146✔
282

146✔
283
                                // Add authorization middleware to router
146✔
284
                                rt2.Use(authorization.Middleware(svr.cfgManager, svr.metricsCl))
146✔
285

146✔
286
                                // Check if GET action is enabled
146✔
287
                                if tgt.Actions.GET != nil && tgt.Actions.GET.Enabled {
292✔
288
                                        // Add GET method to router
146✔
289
                                        rt2.Get("/*", func(rw http.ResponseWriter, req *http.Request) {
191✔
290
                                                // Get bucket request context
45✔
291
                                                brctx := bucket.GetBucketRequestContextFromContext(req.Context())
45✔
292
                                                // Get response handler
45✔
293
                                                resHan := responsehandler.GetResponseHandlerFromContext(req.Context())
45✔
294

45✔
295
                                                // Get request path
45✔
296
                                                requestPath := chi.URLParam(req, "*")
45✔
297

45✔
298
                                                // Unescape it
45✔
299
                                                // Found a bug where sometimes the request path isn't unescaped
45✔
300
                                                requestPath, err := url.PathUnescape(requestPath)
45✔
301
                                                // Check error
45✔
302
                                                if err != nil {
45✔
303
                                                        resHan.InternalServerError(brctx.LoadFileContent, errors.WithStack(err))
×
304

×
305
                                                        return
×
306
                                                }
×
307

308
                                                // Get ETag headers
309

310
                                                // Get If-Modified-Since as string
311
                                                ifModifiedSinceStr := req.Header.Get("If-Modified-Since")
45✔
312
                                                // Create result
45✔
313
                                                var ifModifiedSince *time.Time
45✔
314
                                                // Check if content exists
45✔
315
                                                if ifModifiedSinceStr != "" {
45✔
316
                                                        // Parse time
×
317
                                                        ifModifiedSinceTime, err := http.ParseTime(ifModifiedSinceStr)
×
318
                                                        // Check error
×
319
                                                        if err != nil {
×
320
                                                                resHan.BadRequestError(brctx.LoadFileContent, errors.WithStack(err))
×
321

×
322
                                                                return
×
323
                                                        }
×
324
                                                        // Save result
325
                                                        ifModifiedSince = &ifModifiedSinceTime
×
326
                                                }
327

328
                                                // Get Range
329
                                                byteRange := req.Header.Get("Range")
45✔
330

45✔
331
                                                // Get If-Match
45✔
332
                                                ifMatch := req.Header.Get("If-Match")
45✔
333

45✔
334
                                                // Get If-None-Match
45✔
335
                                                ifNoneMatch := req.Header.Get("If-None-Match")
45✔
336

45✔
337
                                                // Get If-Unmodified-Since as string
45✔
338
                                                ifUnmodifiedSinceStr := req.Header.Get("If-Unmodified-Since")
45✔
339
                                                // Create result
45✔
340
                                                var ifUnmodifiedSince *time.Time
45✔
341
                                                // Check if content exists
45✔
342
                                                if ifUnmodifiedSinceStr != "" {
45✔
343
                                                        // Parse time
×
344
                                                        ifUnmodifiedSinceTime, err := http.ParseTime(ifUnmodifiedSinceStr)
×
345
                                                        // Check error
×
346
                                                        if err != nil {
×
347
                                                                resHan.BadRequestError(brctx.LoadFileContent, errors.WithStack(err))
×
348

×
349
                                                                return
×
350
                                                        }
×
351
                                                        // Save result
352
                                                        ifUnmodifiedSince = &ifUnmodifiedSinceTime
×
353
                                                }
354

355
                                                // Proxy GET Request
356
                                                brctx.Get(req.Context(), &bucket.GetInput{
45✔
357
                                                        RequestPath:       requestPath,
45✔
358
                                                        IfModifiedSince:   ifModifiedSince,
45✔
359
                                                        IfMatch:           ifMatch,
45✔
360
                                                        IfNoneMatch:       ifNoneMatch,
45✔
361
                                                        IfUnmodifiedSince: ifUnmodifiedSince,
45✔
362
                                                        Range:             byteRange,
45✔
363
                                                })
45✔
364
                                        })
365
                                }
366

367
                                // Check if PUT action is enabled
368
                                if tgt.Actions.PUT != nil && tgt.Actions.PUT.Enabled {
154✔
369
                                        // Add PUT method to router
8✔
370
                                        rt2.Put("/*", func(rw http.ResponseWriter, req *http.Request) {
16✔
371
                                                // Get bucket request context
8✔
372
                                                brctx := bucket.GetBucketRequestContextFromContext(req.Context())
8✔
373
                                                // Get response handler
8✔
374
                                                resHan := responsehandler.GetResponseHandlerFromContext(req.Context())
8✔
375

8✔
376
                                                // Get request path
8✔
377
                                                requestPath := chi.URLParam(req, "*")
8✔
378
                                                // Unescape it
8✔
379
                                                // Found a bug where sometimes the request path isn't unescaped
8✔
380
                                                requestPath, err := url.PathUnescape(requestPath)
8✔
381
                                                // Check error
8✔
382
                                                if err != nil {
8✔
383
                                                        resHan.InternalServerError(brctx.LoadFileContent, errors.WithStack(err))
×
384

×
385
                                                        return
×
386
                                                }
×
387

388
                                                // Parse form
389
                                                err = req.ParseForm()
8✔
390
                                                // Check error
8✔
391
                                                if err != nil {
9✔
392
                                                        resHan.InternalServerError(brctx.LoadFileContent, errors.WithStack(err))
1✔
393

1✔
394
                                                        return
1✔
395
                                                }
1✔
396

397
                                                // Parse multipart form
398
                                                err = req.ParseMultipartForm(0)
7✔
399
                                                if err != nil {
7✔
400
                                                        resHan.InternalServerError(brctx.LoadFileContent, errors.WithStack(err))
×
401

×
402
                                                        return
×
403
                                                }
×
404
                                                // Get file from form
405
                                                file, fileHeader, err := req.FormFile("file")
7✔
406
                                                if err != nil {
8✔
407
                                                        resHan.InternalServerError(brctx.LoadFileContent, errors.WithStack(err))
1✔
408

1✔
409
                                                        return
1✔
410
                                                }
1✔
411
                                                // Defer close file
412
                                                defer file.Close()
6✔
413
                                                // Defer remove all form
6✔
414
                                                defer req.MultipartForm.RemoveAll() //nolint: errcheck // Ignored
6✔
415

6✔
416
                                                // Create input for put request
6✔
417
                                                inp := &bucket.PutInput{
6✔
418
                                                        RequestPath: requestPath,
6✔
419
                                                        Filename:    fileHeader.Filename,
6✔
420
                                                        Body:        file,
6✔
421
                                                        ContentType: fileHeader.Header.Get("Content-Type"),
6✔
422
                                                        ContentSize: fileHeader.Size,
6✔
423
                                                }
6✔
424
                                                // Action
6✔
425
                                                brctx.Put(req.Context(), inp)
6✔
426
                                        })
427
                                }
428

429
                                // Check if DELETE action is enabled
430
                                if tgt.Actions.DELETE != nil && tgt.Actions.DELETE.Enabled {
149✔
431
                                        // Add DELETE method to router
3✔
432
                                        rt2.Delete("/*", func(rw http.ResponseWriter, req *http.Request) {
6✔
433
                                                // Get bucket request context
3✔
434
                                                brctx := bucket.GetBucketRequestContextFromContext(req.Context())
3✔
435
                                                // Get response handler
3✔
436
                                                resHan := responsehandler.GetResponseHandlerFromContext(req.Context())
3✔
437
                                                // Get request path
3✔
438
                                                requestPath := chi.URLParam(req, "*")
3✔
439
                                                // Unescape it
3✔
440
                                                // Found a bug where sometimes the request path isn't unescaped
3✔
441
                                                requestPath, err := url.PathUnescape(requestPath)
3✔
442
                                                // Check error
3✔
443
                                                if err != nil {
3✔
444
                                                        resHan.InternalServerError(brctx.LoadFileContent, errors.WithStack(err))
×
445

×
446
                                                        return
×
447
                                                }
×
448
                                                // Proxy DELETE Request
449
                                                brctx.Delete(req.Context(), requestPath)
3✔
450
                                        })
451
                                }
452
                        })
453
                })
454
                // Mount domain from target
455
                hr.Map(domain, rt)
146✔
456
        }
457

458
        // Mount host router
459
        r.Mount("/", hr)
107✔
460

107✔
461
        return r, nil
107✔
462
}
463

464
// Generate CORS.
465
func generateCors(cfg *config.ServerConfig, logger log.CorsLogger) *cors.Cors {
6✔
466
        // Check if allow all is enabled
6✔
467
        if cfg.CORS.AllowAll {
7✔
468
                cc := cors.AllowAll()
1✔
469
                // Add logger
1✔
470
                cc.Log = logger
1✔
471
                // Return
1✔
472
                return cc
1✔
473
        }
1✔
474

475
        // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing
476
        corsOpt := cors.Options{}
5✔
477
        // Check if allowed origins exist
5✔
478
        if cfg.CORS.AllowOrigins != nil {
10✔
479
                corsOpt.AllowedOrigins = cfg.CORS.AllowOrigins
5✔
480
        }
5✔
481
        // Check if allowed methods exist
482
        if cfg.CORS.AllowMethods != nil {
10✔
483
                corsOpt.AllowedMethods = cfg.CORS.AllowMethods
5✔
484
        }
5✔
485
        // Check if allowed headers exist
486
        if cfg.CORS.AllowHeaders != nil {
5✔
487
                corsOpt.AllowedHeaders = cfg.CORS.AllowHeaders
×
488
        }
×
489
        // Check if exposed headers exist
490
        if cfg.CORS.ExposeHeaders != nil {
5✔
491
                corsOpt.ExposedHeaders = cfg.CORS.ExposeHeaders
×
492
        }
×
493
        // Check if allow credentials exist
494
        if cfg.CORS.AllowCredentials != nil {
5✔
495
                corsOpt.AllowCredentials = *cfg.CORS.AllowCredentials
×
496
        }
×
497
        // Check if max age exists
498
        // 300 = Maximum value not ignored by any of major browsers
499
        if cfg.CORS.MaxAge != nil {
5✔
500
                corsOpt.MaxAge = *cfg.CORS.MaxAge
×
501
        }
×
502
        // Check if debug option exists
503
        if cfg.CORS.Debug != nil {
5✔
504
                corsOpt.Debug = *cfg.CORS.Debug
×
505
        }
×
506
        // Check if Options Passthrough exists
507
        if cfg.CORS.OptionsPassthrough != nil {
5✔
508
                corsOpt.OptionsPassthrough = *cfg.CORS.OptionsPassthrough
×
509
        }
×
510

511
        cc := cors.New(corsOpt)
5✔
512
        // Add logger
5✔
513
        cc.Log = logger
5✔
514
        // Return
5✔
515
        return cc
5✔
516
}
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