-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(server/v2): Add Swagger UI support for server/v2 #23092
base: main
Are you sure you want to change the base?
Changes from all commits
4ed5650
2ac2555
8930230
c812722
d0c20bc
ac5982a
f96dafe
9859c38
e96e5bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package swagger | ||
|
||
import ( | ||
"fmt" | ||
|
||
"cosmossdk.io/core/server" | ||
) | ||
|
||
const ServerName = "swagger" | ||
|
||
// Config defines the configuration for the Swagger UI server | ||
type Config struct { | ||
Enable bool `toml:"enable" mapstructure:"enable"` | ||
Address string `toml:"address" mapstructure:"address"` | ||
Path string `toml:"path" mapstructure:"path"` | ||
} | ||
|
||
// DefaultConfig returns the default configuration | ||
func DefaultConfig() *Config { | ||
return &Config{ | ||
Enable: true, | ||
Address: "localhost:8080", | ||
Path: "/swagger/", | ||
} | ||
} | ||
|
||
// Validate checks the configuration | ||
func (c Config) Validate() error { | ||
if c.Path == "" { | ||
return fmt.Errorf("swagger path cannot be empty") | ||
} | ||
return nil | ||
} | ||
|
||
// CfgOption defines a function for configuring the settings | ||
type CfgOption func(*Config) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package swagger | ||
|
||
import ( | ||
"io" | ||
"net/http" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/rakyll/statik/fs" | ||
) | ||
|
||
// Handler returns an HTTP handler for Swagger UI | ||
func Handler() http.Handler { | ||
return &swaggerHandler{} | ||
} | ||
|
||
type swaggerHandler struct{} | ||
|
||
func (h *swaggerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
// Set CORS headers | ||
w.Header().Set("Access-Control-Allow-Origin", "*") | ||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") | ||
w.Header().Set("Access-Control-Allow-Headers", "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type") | ||
|
||
if r.Method == http.MethodOptions { | ||
return | ||
} | ||
|
||
// Get the static file system | ||
statikFS, err := fs.New() | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
// Process the path | ||
urlPath := strings.TrimPrefix(r.URL.Path, "/swagger") | ||
if urlPath == "" || urlPath == "/" { | ||
urlPath = "/index.html" | ||
} | ||
|
||
// Open the file | ||
file, err := statikFS.Open(urlPath) | ||
if err != nil { | ||
http.Error(w, "File not found", http.StatusNotFound) | ||
return | ||
} | ||
defer file.Close() | ||
|
||
// Set the content-type | ||
ext := filepath.Ext(urlPath) | ||
if ct := getContentType(ext); ct != "" { | ||
w.Header().Set("Content-Type", ct) | ||
} | ||
|
||
// Set caching headers | ||
w.Header().Set("Cache-Control", "public, max-age=31536000") | ||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) | ||
Check warning Code scanning / CodeQL Calling the system time Warning
Calling the system time may be a possible source of non-determinism
|
||
|
||
// Serve the file | ||
http.ServeContent(w, r, urlPath, time.Now(), file.(io.ReadSeeker)) | ||
Check warning Code scanning / CodeQL Calling the system time Warning
Calling the system time may be a possible source of non-determinism
|
||
} | ||
|
||
// getContentType returns the content-type for a file extension | ||
func getContentType(ext string) string { | ||
switch strings.ToLower(ext) { | ||
case ".html": | ||
return "text/html" | ||
case ".css": | ||
return "text/css" | ||
case ".js": | ||
return "application/javascript" | ||
case ".json": | ||
return "application/json" | ||
case ".png": | ||
return "image/png" | ||
case ".jpg", ".jpeg": | ||
return "image/jpeg" | ||
case ".svg": | ||
return "image/svg+xml" | ||
default: | ||
return "" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,108 @@ | ||||||
package swagger | ||||||
|
||||||
import ( | ||||||
"context" | ||||||
"fmt" | ||||||
"net/http" | ||||||
|
||||||
"cosmossdk.io/core/server" | ||||||
"cosmossdk.io/core/transaction" | ||||||
"cosmossdk.io/log" | ||||||
serverv2 "cosmossdk.io/server/v2" | ||||||
) | ||||||
|
||||||
var ( | ||||||
_ serverv2.ServerComponent[transaction.Tx] = (*Server[transaction.Tx])(nil) | ||||||
_ serverv2.HasConfig = (*Server[transaction.Tx])(nil) | ||||||
) | ||||||
|
||||||
// Server represents a Swagger UI server | ||||||
type Server[T transaction.Tx] struct { | ||||||
logger log.Logger | ||||||
config *Config | ||||||
cfgOptions []CfgOption | ||||||
server *http.Server | ||||||
} | ||||||
|
||||||
// New creates a new Swagger UI server | ||||||
func New[T transaction.Tx]( | ||||||
logger log.Logger, | ||||||
cfg server.ConfigMap, | ||||||
cfgOptions ...CfgOption, | ||||||
) (*Server[T], error) { | ||||||
srv := &Server[T]{ | ||||||
logger: logger.With(log.ModuleKey, ServerName), | ||||||
cfgOptions: cfgOptions, | ||||||
} | ||||||
|
||||||
serverCfg := srv.Config().(*Config) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the undefined method call. Static analysis indicates that Here’s a suggested fix: - serverCfg := srv.Config().(*Config)
+ serverCfg := srv.config 📝 Committable suggestion
Suggested change
🧰 Tools🪛 golangci-lint (1.62.2)31-31: srv.Config undefined (type *Server[T] has no field or method Config, but does have field config) (typecheck) |
||||||
if len(cfg) > 0 { | ||||||
if err := serverv2.UnmarshalSubConfig(cfg, srv.Name(), &serverCfg); err != nil { | ||||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err) | ||||||
} | ||||||
} | ||||||
srv.config = serverCfg | ||||||
|
||||||
if err := srv.config.Validate(); err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
mux := http.NewServeMux() | ||||||
mux.Handle(srv.config.Path, Handler()) | ||||||
|
||||||
srv.server = &http.Server{ | ||||||
Addr: srv.config.Address, | ||||||
Handler: mux, | ||||||
} | ||||||
|
||||||
return srv, nil | ||||||
} | ||||||
|
||||||
// NewWithConfigOptions creates a new server with configuration options | ||||||
func NewWithConfigOptions[T transaction.Tx](opts ...CfgOption) *Server[T] { | ||||||
return &Server[T]{ | ||||||
cfgOptions: opts, | ||||||
} | ||||||
} | ||||||
|
||||||
// Name returns the server's name | ||||||
func (s *Server[T]) Name() string { | ||||||
return ServerName | ||||||
} | ||||||
|
||||||
// Config returns the server configuration | ||||||
func (s *Server[T]) Config() any { | ||||||
if s.config == nil || s.config.Address == "" { | ||||||
cfg := DefaultConfig() | ||||||
for _, opt := range s.cfgOptions { | ||||||
opt(cfg) | ||||||
} | ||||||
return cfg | ||||||
} | ||||||
return s.config | ||||||
} | ||||||
|
||||||
// Start starts the server | ||||||
func (s *Server[T]) Start(ctx context.Context) error { | ||||||
if !s.config.Enable { | ||||||
s.logger.Info(fmt.Sprintf("%s server is disabled via config", s.Name())) | ||||||
return nil | ||||||
} | ||||||
|
||||||
s.logger.Info("starting swagger server...", "address", s.config.Address) | ||||||
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||||||
return fmt.Errorf("failed to start swagger server: %w", err) | ||||||
} | ||||||
|
||||||
return nil | ||||||
} | ||||||
|
||||||
// Stop stops the server | ||||||
func (s *Server[T]) Stop(ctx context.Context) error { | ||||||
if !s.config.Enable { | ||||||
return nil | ||||||
} | ||||||
|
||||||
s.logger.Info("stopping swagger server...", "address", s.config.Address) | ||||||
return s.server.Shutdown(ctx) | ||||||
} |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High