mirror of
https://github.com/bitmagnet-io/bitmagnet.git
synced 2026-07-31 16:33:49 -04:00
Plugins WIP
This commit is contained in:
@@ -3,61 +3,13 @@ package httpserver
|
||||
type Config struct {
|
||||
LocalAddress string
|
||||
GinMode string
|
||||
Cors CorsConfig
|
||||
Options []string
|
||||
}
|
||||
|
||||
type CorsConfig struct {
|
||||
// AllowedOrigins is a list of origins a cross-domain request can be executed from.
|
||||
// If the special "*" value is present in the list, all origins will be allowed.
|
||||
// An origin may contain a wildcard (*) to replace 0 or more characters
|
||||
// (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty.
|
||||
// Only one wildcard can be used per origin.
|
||||
// Default value is ["*"]
|
||||
AllowedOrigins []string
|
||||
// AllowedMethods is a list of methods the client is allowed to use with
|
||||
// cross-domain requests. Default value is simple methods (HEAD, GET and POST).
|
||||
AllowedMethods []string
|
||||
// AllowedHeaders is list of non simple headers the client is allowed to use with
|
||||
// cross-domain requests.
|
||||
// If the special "*" value is present in the list, all headers will be allowed.
|
||||
// Default value is [].
|
||||
AllowedHeaders []string
|
||||
// ExposedHeaders indicates which headers are safe to expose to the API of a CORS
|
||||
// API specification
|
||||
ExposedHeaders []string
|
||||
// MaxAge indicates how long (in seconds) the results of a preflight request
|
||||
// can be cached. Default value is 0, which stands for no
|
||||
// Access-Control-Max-Age header to be sent back, resulting in browsers
|
||||
// using their default value (5s by spec). If you need to force a 0 max-age,
|
||||
// set `MaxAge` to a negative value (ie: -1).
|
||||
MaxAge int
|
||||
// AllowCredentials indicates whether the request can include user credentials like
|
||||
// cookies, HTTP authentication or client side SSL certificates.
|
||||
AllowCredentials bool
|
||||
// AllowPrivateNetwork indicates whether to accept cross-origin requests over a
|
||||
// private network.
|
||||
AllowPrivateNetwork bool
|
||||
// OptionsPassthrough instructs preflight to let other potential next handlers to
|
||||
// process the OPTIONS method. Turn this on if your application handles OPTIONS.
|
||||
OptionsPassthrough bool
|
||||
// Provides a status code to use for successful OPTIONS requests.
|
||||
// Default value is http.StatusNoContent (204).
|
||||
OptionsSuccessStatus int
|
||||
// Debugging flag adds additional output to debug server side CORS issues
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func NewDefaultConfig() Config {
|
||||
return Config{
|
||||
LocalAddress: ":3333",
|
||||
GinMode: "release",
|
||||
// todo review
|
||||
Cors: CorsConfig{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedHeaders: []string{"*"},
|
||||
Debug: true,
|
||||
},
|
||||
Options: []string{"*"},
|
||||
Options: []string{"*"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/httpserver"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/cors"
|
||||
gincors "github.com/rs/cors/wrapper/gin"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func New(
|
||||
config httpserver.CorsConfig,
|
||||
logger *zap.SugaredLogger,
|
||||
) httpserver.Option {
|
||||
return corsOption{gincors.New(cors.Options{
|
||||
AllowedOrigins: config.AllowedOrigins,
|
||||
AllowedMethods: config.AllowedMethods,
|
||||
AllowedHeaders: config.AllowedHeaders,
|
||||
ExposedHeaders: config.ExposedHeaders,
|
||||
MaxAge: config.MaxAge,
|
||||
AllowCredentials: config.AllowCredentials,
|
||||
AllowPrivateNetwork: config.AllowPrivateNetwork,
|
||||
OptionsPassthrough: config.OptionsPassthrough,
|
||||
OptionsSuccessStatus: config.OptionsSuccessStatus,
|
||||
Debug: config.Debug,
|
||||
// we don't need every request logged so apply sampling
|
||||
Logger: corsLogger{logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
||||
return zapcore.NewSamplerWithOptions(core, time.Hour, 10, 0)
|
||||
})).Named("cors")},
|
||||
})}
|
||||
}
|
||||
|
||||
type corsOption struct {
|
||||
handlerFunc gin.HandlerFunc
|
||||
}
|
||||
|
||||
func (corsOption) Key() string {
|
||||
return "cors"
|
||||
}
|
||||
|
||||
func (c corsOption) Apply(g *gin.Engine) {
|
||||
g.Use(c.handlerFunc)
|
||||
}
|
||||
|
||||
type corsLogger struct {
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (c corsLogger) Printf(format string, v ...interface{}) {
|
||||
c.logger.Debugf(format, v...)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 gin-contrib
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copied from https://github.com/gin-contrib/zap
|
||||
// In order to customize log level
|
||||
|
||||
package ginzap
|
||||
@@ -1,138 +0,0 @@
|
||||
package ginzap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
func buildDummyLogger() (*zap.Logger, *observer.ObservedLogs) {
|
||||
core, obs := observer.New(zap.DebugLevel)
|
||||
logger := zap.New(core)
|
||||
|
||||
return logger, obs
|
||||
}
|
||||
|
||||
func timestampLocationCheck(timestampStr string, location *time.Location) error {
|
||||
timestamp, err := time.Parse(time.RFC3339, timestampStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if timestamp.Location() != location {
|
||||
return fmt.Errorf("timestamp should be utc but %v", timestamp.Location())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const testPath = "/test"
|
||||
|
||||
func TestGinzap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := gin.New()
|
||||
|
||||
utcLogger, utcLoggerObserved := buildDummyLogger()
|
||||
r.Use(Ginzap(utcLogger, time.RFC3339, true))
|
||||
|
||||
localLogger, localLoggerObserved := buildDummyLogger()
|
||||
r.Use(Ginzap(localLogger, time.RFC3339, false))
|
||||
|
||||
r.GET(testPath, func(c *gin.Context) {
|
||||
c.JSON(204, nil)
|
||||
})
|
||||
|
||||
res1 := httptest.NewRecorder()
|
||||
|
||||
ctx := t.Context()
|
||||
req1, _ := http.NewRequestWithContext(ctx, http.MethodGet, testPath, nil)
|
||||
r.ServeHTTP(res1, req1)
|
||||
|
||||
if len(utcLoggerObserved.All()) != 1 {
|
||||
t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All()))
|
||||
}
|
||||
|
||||
logLine := utcLoggerObserved.All()[0]
|
||||
|
||||
pathStr := logLine.Context[2].String
|
||||
if pathStr != testPath {
|
||||
t.Fatalf("logged path should be /test but %s", pathStr)
|
||||
}
|
||||
|
||||
err := timestampLocationCheck(logLine.Context[7].String, time.UTC)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(localLoggerObserved.All()) != 1 {
|
||||
t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All()))
|
||||
}
|
||||
|
||||
logLine = localLoggerObserved.All()[0]
|
||||
|
||||
pathStr = logLine.Context[2].String
|
||||
if pathStr != testPath {
|
||||
t.Fatalf("logged path should be /test but %s", pathStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGinzapWithConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := gin.New()
|
||||
|
||||
utcLogger, utcLoggerObserved := buildDummyLogger()
|
||||
r.Use(WithConfig(utcLogger, &Config{
|
||||
TimeFormat: time.RFC3339,
|
||||
UTC: true,
|
||||
SkipPaths: []string{"/no_log"},
|
||||
}))
|
||||
|
||||
r.GET(testPath, func(c *gin.Context) {
|
||||
c.JSON(204, nil)
|
||||
})
|
||||
|
||||
r.GET("/no_log", func(c *gin.Context) {
|
||||
c.JSON(204, nil)
|
||||
})
|
||||
|
||||
res1 := httptest.NewRecorder()
|
||||
ctx := t.Context()
|
||||
req1, _ := http.NewRequestWithContext(ctx, http.MethodGet, testPath, nil)
|
||||
r.ServeHTTP(res1, req1)
|
||||
|
||||
res2 := httptest.NewRecorder()
|
||||
req2, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/no_log", nil)
|
||||
r.ServeHTTP(res2, req2)
|
||||
|
||||
if res2.Code != 204 {
|
||||
t.Fatalf("request /no_log is failed (%d)", res2.Code)
|
||||
}
|
||||
|
||||
if len(utcLoggerObserved.All()) != 1 {
|
||||
t.Fatalf("Log should be 1 line but there're %d", len(utcLoggerObserved.All()))
|
||||
}
|
||||
|
||||
logLine := utcLoggerObserved.All()[0]
|
||||
|
||||
pathStr := logLine.Context[2].String
|
||||
if pathStr != testPath {
|
||||
t.Fatalf("logged path should be /test but %s", pathStr)
|
||||
}
|
||||
|
||||
err := timestampLocationCheck(logLine.Context[7].String, time.UTC)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package ginzap
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/httpserver"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Option(logger *zap.Logger) httpserver.Option {
|
||||
return httpserver.NewOption("logger", func(engine *gin.Engine) {
|
||||
engine.Use(Ginzap(logger, time.RFC3339, true))
|
||||
})
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package httpserverfx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/blocking"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/config/configfx"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/database"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/health"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/httpserver"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/httpserver/circuitbreaker"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/httpserver/cors"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/httpserver/ginzap"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/workers/registry"
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/workers/worker"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func New() fx.Option {
|
||||
return fx.Module(
|
||||
httpserver.Namespace,
|
||||
configfx.NewConfigModule[httpserver.Config](httpserver.Namespace, httpserver.NewDefaultConfig()),
|
||||
fx.Provide(
|
||||
circuitbreaker.New,
|
||||
fx.Annotate(
|
||||
func(logger *zap.Logger) httpserver.Option {
|
||||
return httpserver.NewOption("logger", func(engine *gin.Engine) {
|
||||
engine.Use(ginzap.Ginzap(logger, time.RFC3339, true))
|
||||
})
|
||||
},
|
||||
fx.ResultTags(`group:"http_server_options"`),
|
||||
),
|
||||
fx.Annotate(
|
||||
func() httpserver.Option {
|
||||
return httpserver.NewOption("recovery", func(engine *gin.Engine) {
|
||||
engine.Use(gin.Recovery())
|
||||
})
|
||||
},
|
||||
fx.ResultTags(`group:"http_server_options"`),
|
||||
),
|
||||
fx.Annotate(
|
||||
func(
|
||||
options []httpserver.Option,
|
||||
config httpserver.Config,
|
||||
) (gin.OptionFunc, error) {
|
||||
return resolveOptions(options, config.Options)
|
||||
},
|
||||
fx.ParamTags(`group:"http_server_options"`),
|
||||
),
|
||||
fx.Annotate(
|
||||
func(
|
||||
config httpserver.Config,
|
||||
handler circuitbreaker.Handler,
|
||||
) (registry.Option, error) {
|
||||
return registry.WithWorker(
|
||||
httpserver.Namespace,
|
||||
httpserver.New(handler, config.LocalAddress),
|
||||
worker.WithDependencies(
|
||||
database.Namespace,
|
||||
blocking.Namespace,
|
||||
health.Namespace,
|
||||
),
|
||||
), nil
|
||||
},
|
||||
fx.ResultTags(`group:"worker_options"`),
|
||||
),
|
||||
fx.Annotate(
|
||||
func(
|
||||
config httpserver.Config,
|
||||
logger *zap.SugaredLogger,
|
||||
) httpserver.Option {
|
||||
return cors.New(config.Cors, logger)
|
||||
},
|
||||
fx.ResultTags(`group:"http_server_options"`),
|
||||
),
|
||||
),
|
||||
fx.Invoke(
|
||||
func(
|
||||
config httpserver.Config,
|
||||
option gin.OptionFunc,
|
||||
handler circuitbreaker.Handler,
|
||||
) error {
|
||||
gin.SetMode(config.GinMode)
|
||||
|
||||
return handler.SetOption(option)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func resolveOptions(options []httpserver.Option, optionsNames []string) (gin.OptionFunc, error) {
|
||||
paramMap := make(map[string]struct{})
|
||||
for _, p := range optionsNames {
|
||||
paramMap[p] = struct{}{}
|
||||
}
|
||||
|
||||
enabledOptions := make([]httpserver.Option, 0, len(options))
|
||||
|
||||
foundMap := make(map[string]struct{}, len(options))
|
||||
for _, o := range options {
|
||||
if _, ok := foundMap[o.Key()]; ok {
|
||||
return nil, fmt.Errorf("duplicate http server option: '%s'", o.Key())
|
||||
}
|
||||
|
||||
foundMap[o.Key()] = struct{}{}
|
||||
|
||||
enabled := false
|
||||
if _, ok := paramMap["*"]; ok {
|
||||
enabled = true
|
||||
} else if _, ok := paramMap[o.Key()]; ok {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
if enabled {
|
||||
enabledOptions = append(enabledOptions, o)
|
||||
}
|
||||
}
|
||||
|
||||
for p := range paramMap {
|
||||
if _, ok := foundMap[p]; !ok && p != "*" {
|
||||
return nil, fmt.Errorf("unknown http server option: '%s'", p)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(enabledOptions, func(i, j int) bool {
|
||||
return enabledOptions[i].Key() < enabledOptions[j].Key()
|
||||
})
|
||||
|
||||
return func(engine *gin.Engine) {
|
||||
for _, opt := range enabledOptions {
|
||||
opt.Apply(engine)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"github.com/bitmagnet-io/bitmagnet/internal/workers/runner"
|
||||
)
|
||||
|
||||
const Namespace = "http_server"
|
||||
|
||||
func New(
|
||||
handler http.Handler,
|
||||
localAddress string,
|
||||
|
||||
Reference in New Issue
Block a user