[assetserver] Introduce middleware and extract options (#2016)

* [assetserver] Add support for HTTP Middlewares

* [dev] Disable frontend DevServer if no Assets has been defined and inform user

* [dev] Consistent WebSocket behaviour in dev and prod mode for assets handler and middleware

In prod mode we can't support WebSockets so make sure the
assets handler and middleware never see WebSockets in dev mode.

* [templates] Migrate to new AssetServer option

* [docs] Add assetserver.Options to the reference
This commit is contained in:
stffabi
2022-10-29 23:15:15 +02:00
committed by GitHub
parent 4cd89e1507
commit 638caf72f0
37 changed files with 397 additions and 150 deletions
@@ -7,6 +7,7 @@ import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
@@ -36,7 +37,9 @@ func main() {
StartHidden: false,
HideWindowOnClose: false,
BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255},
Assets: assets,
AssetServer: &assetserver.Options{
Assets: assets,
},
Menu: nil,
Logger: nil,
LogLevel: logger.DEBUG,
@@ -9,6 +9,7 @@ import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
@@ -37,7 +38,9 @@ func main() {
StartHidden: false,
HideWindowOnClose: false,
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
Assets: assets,
AssetServer: &assetserver.Options{
Assets: assets,
},
Menu: nil,
Logger: nil,
LogLevel: logger.DEBUG,
@@ -5,6 +5,7 @@ import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
@@ -16,10 +17,12 @@ func main() {
// Create application with options
err := wails.Run(&options.App{
Title: "{{.ProjectName}}",
Width: 1024,
Height: 768,
Assets: assets,
Title: "{{.ProjectName}}",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
+16 -2
View File
@@ -13,6 +13,7 @@ import (
"path/filepath"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
"github.com/wailsapp/wails/v2/internal/frontend/desktop"
"github.com/wailsapp/wails/v2/internal/frontend/devserver"
"github.com/wailsapp/wails/v2/internal/frontend/dispatcher"
@@ -90,6 +91,14 @@ func CreateApp(appoptions *options.App) (*App, error) {
}
}
assetConfig := assetserver.BuildAssetServerConfig(appoptions)
if assetConfig.Assets == nil && frontendDevServerURL != "" {
myLogger.Warning("No AssetServer.Assets has been defined but a frontend DevServer, the frontend DevServer will not be used.")
frontendDevServerURL = ""
assetdir = ""
}
if frontendDevServerURL != "" {
if devServer == "" {
return nil, fmt.Errorf("Unable to use FrontendDevServerUrl without a DevServer address")
@@ -107,7 +116,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
} else {
if assetdir == "" {
// If no assetdir has been defined, let's try to infer it from the project root and the asset FS.
assetdir, err = tryInferAssetDirFromFS(appoptions.Assets)
assetdir, err = tryInferAssetDirFromFS(assetConfig.Assets)
if err != nil {
return nil, err
}
@@ -121,12 +130,17 @@ func CreateApp(appoptions *options.App) (*App, error) {
}
myLogger.Info("Serving assets from disk: %s", absdir)
appoptions.Assets = os.DirFS(absdir)
assetConfig.Assets = os.DirFS(absdir)
ctx = context.WithValue(ctx, "assetdir", assetdir)
}
}
// Migrate deprecated options to the new AssetServer option
appoptions.Assets = nil
appoptions.AssetsHandler = nil
appoptions.AssetServer = &assetConfig
if devServer != "" {
ctx = context.WithValue(ctx, "devserver", devServer)
}
@@ -14,6 +14,7 @@ import (
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed defaultindex.html
@@ -32,7 +33,13 @@ type assetHandler struct {
retryMissingFiles bool
}
func NewAssetHandler(ctx context.Context, vfs iofs.FS, assetsHandler http.Handler) (http.Handler, error) {
func NewAssetHandler(ctx context.Context, options assetserver.Options) (http.Handler, error) {
var log *logger.Logger
if _logger := ctx.Value("logger"); _logger != nil {
log = _logger.(*logger.Logger)
}
vfs := options.Assets
if vfs != nil {
if _, err := vfs.Open("."); err != nil {
return nil, err
@@ -49,13 +56,14 @@ func NewAssetHandler(ctx context.Context, vfs iofs.FS, assetsHandler http.Handle
}
}
result := &assetHandler{
var result http.Handler = &assetHandler{
fs: vfs,
handler: assetsHandler,
handler: options.Handler,
logger: log,
}
if _logger := ctx.Value("logger"); _logger != nil {
result.logger = _logger.(*logger.Logger)
if middleware := options.Middleware; middleware != nil {
result = middleware(result)
}
return result, nil
+21 -11
View File
@@ -4,15 +4,16 @@ import (
"bytes"
"context"
"fmt"
iofs "io/fs"
"net/http"
"net/http/httptest"
"strconv"
"golang.org/x/net/html"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"golang.org/x/net/html"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
const (
@@ -22,6 +23,7 @@ const (
type AssetServer struct {
handler http.Handler
wsHandler http.Handler
runtimeJS []byte
ipcJS func(*http.Request) []byte
@@ -31,8 +33,12 @@ type AssetServer struct {
appendSpinnerToBody bool
}
func NewAssetServer(ctx context.Context, vfs iofs.FS, assetsHandler http.Handler, bindingsJSON string) (*AssetServer, error) {
handler, err := NewAssetHandler(ctx, vfs, assetsHandler)
func NewAssetServerMainPage(ctx context.Context, bindingsJSON string, options *options.App) (*AssetServer, error) {
return NewAssetServer(ctx, bindingsJSON, BuildAssetServerConfig(options))
}
func NewAssetServer(ctx context.Context, bindingsJSON string, options assetserver.Options) (*AssetServer, error) {
handler, err := NewAssetHandler(ctx, options)
if err != nil {
return nil, err
}
@@ -66,17 +72,21 @@ func NewAssetServerWithHandler(ctx context.Context, handler http.Handler, bindin
}
func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if isWebSocket(req) {
// Forward WebSockets to the distinct websocket handler if it exists
if wsHandler := d.wsHandler; wsHandler != nil {
wsHandler.ServeHTTP(rw, req)
} else {
rw.WriteHeader(http.StatusNotImplemented)
}
return
}
header := rw.Header()
if d.servingFromDisk {
header.Add(HeaderCacheControl, "no-cache")
}
if isWebSocket(req) {
// WebSockets can always directly be forwarded to the handler
d.handler.ServeHTTP(rw, req)
return
}
path := req.URL.Path
switch path {
case "", "/", "/index.html":
@@ -12,15 +12,17 @@ import (
)
/*
The assetserver for dev serves assets from disk.
It injects a websocket based IPC script into `index.html`.
The assetserver for the dev mode.
Depending on the UserAgent it injects a websocket based IPC script into `index.html` or the default desktop IPC. The
default desktop IPC is injected when the webview accesses the devserver.
*/
func NewBrowserAssetServer(ctx context.Context, handler http.Handler, bindingsJSON string) (*AssetServer, error) {
func NewDevAssetServer(ctx context.Context, handler http.Handler, wsHandler http.Handler, bindingsJSON string) (*AssetServer, error) {
result, err := NewAssetServerWithHandler(ctx, handler, bindingsJSON)
if err != nil {
return nil, err
}
result.wsHandler = wsHandler
result.appendSpinnerToBody = true
result.ipcJS = func(req *http.Request) []byte {
if strings.Contains(req.UserAgent(), WailsUserAgentValue) {
@@ -8,9 +8,26 @@ import (
"net/http"
"strings"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"golang.org/x/net/html"
)
func BuildAssetServerConfig(options *options.App) assetserver.Options {
if opts := options.AssetServer; opts != nil {
if options.Assets != nil || options.AssetsHandler != nil {
panic("It's not possible to use the deprecated Assets and AssetsHandler options and the new AssetServer option at the same time. Please migrate all your Assets options to the AssetServer option.")
}
return *opts
}
return assetserver.Options{
Assets: options.Assets,
Handler: options.AssetsHandler,
}
}
const (
HeaderHost = "Host"
HeaderContentType = "Content-Type"
@@ -90,7 +90,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
} else {
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServer(ctx, appoptions.Assets, appoptions.AssetsHandler, bindings)
assets, err := assetserver.NewAssetServerMainPage(ctx, bindings, appoptions)
if err != nil {
log.Fatal(err)
}
@@ -94,7 +94,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
} else {
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServer(ctx, appoptions.Assets, appoptions.AssetsHandler, bindings)
assets, err := assetserver.NewAssetServerMainPage(ctx, bindings, appoptions)
if err != nil {
log.Fatal(err)
}
@@ -102,7 +102,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
appBindings.DB().UpdateObfuscatedCallMap()
}
assets, err := assetserver.NewAssetServer(ctx, appoptions.Assets, appoptions.AssetsHandler, bindings)
assets, err := assetserver.NewAssetServerMainPage(ctx, bindings, appoptions)
if err != nil {
log.Fatal(err)
}
@@ -435,9 +435,8 @@ func (f *Frontend) setupChromium() {
log.Fatal(err)
}
if opts := f.frontendOptions.Windows; opts != nil {
if opts.ZoomFactor > 0.0 {
if opts := f.frontendOptions.Windows; opts != nil {
if opts.ZoomFactor > 0.0 {
chromium.PutZoomFactor(opts.ZoomFactor)
}
err = settings.PutIsZoomControlEnabled(opts.IsZoomControlEnabled)
+11 -3
View File
@@ -12,6 +12,7 @@ import (
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"sync"
@@ -53,7 +54,10 @@ func (d *DevWebServer) Run(ctx context.Context) error {
d.server.GET("/wails/reload", d.handleReload)
d.server.GET("/wails/ipc", d.handleIPCWebSocket)
assetServerConfig := assetserver.BuildAssetServerConfig(d.appoptions)
var assetHandler http.Handler
var wsHandler http.Handler
_fronendDevServerURL, _ := ctx.Value("frontenddevserverurl").(string)
if _fronendDevServerURL == "" {
assetdir, _ := ctx.Value("assetdir").(string)
@@ -62,7 +66,7 @@ func (d *DevWebServer) Run(ctx context.Context) error {
})
var err error
assetHandler, err = assetserver.NewAssetHandler(ctx, d.appoptions.Assets, d.appoptions.AssetsHandler)
assetHandler, err = assetserver.NewAssetHandler(ctx, assetServerConfig)
if err != nil {
log.Fatal(err)
}
@@ -81,7 +85,11 @@ func (d *DevWebServer) Run(ctx context.Context) error {
d.logger.Error("Timeout waiting for frontend DevServer")
}
assetHandler = newExternalDevServerAssetHandler(d.logger, externalURL, d.appoptions.AssetsHandler)
assetHandler = newExternalDevServerAssetHandler(d.logger, externalURL, assetServerConfig)
// WebSockets aren't currently supported in prod mode, so a WebSocket connection is the result of the
// FrontendDevServer e.g. Vite to support auto reloads.
// Therefore we direct WebSockets directly to the FrontendDevServer instead of returning a NotImplementedStatus.
wsHandler = httputil.NewSingleHostReverseProxy(externalURL)
}
// Setup internal dev server
@@ -90,7 +98,7 @@ func (d *DevWebServer) Run(ctx context.Context) error {
log.Fatal(err)
}
assetServer, err := assetserver.NewBrowserAssetServer(ctx, assetHandler, bindingsJSON)
assetServer, err := assetserver.NewDevAssetServer(ctx, assetHandler, wsHandler, bindingsJSON)
if err != nil {
log.Fatal(err)
}
+18 -13
View File
@@ -10,11 +10,21 @@ import (
"net/http/httputil"
"net/url"
"github.com/labstack/echo/v4"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
func newExternalDevServerAssetHandler(logger *logger.Logger, url *url.URL, handler http.Handler) http.Handler {
func newExternalDevServerAssetHandler(logger *logger.Logger, url *url.URL, options assetserver.Options) http.Handler {
handler := newExternalAssetsHandler(logger, url, options.Handler)
if middleware := options.Middleware; middleware != nil {
handler = middleware(handler)
}
return handler
}
func newExternalAssetsHandler(logger *logger.Logger, url *url.URL, handler http.Handler) http.Handler {
errSkipProxy := fmt.Errorf("skip proxying")
proxy := httputil.NewSingleHostReverseProxy(url)
@@ -56,23 +66,18 @@ func newExternalDevServerAssetHandler(logger *logger.Logger, url *url.URL, handl
}
}
e := echo.New()
e.Any("/*",
func(c echo.Context) error {
req := c.Request()
rw := c.Response()
if c.IsWebSocket() || req.Method == http.MethodGet {
return http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet {
proxy.ServeHTTP(rw, req)
return nil
return
}
if handler != nil {
handler.ServeHTTP(rw, req)
return nil
return
}
return c.NoContent(http.StatusMethodNotAllowed)
rw.WriteHeader(http.StatusMethodNotAllowed)
})
return e
}
@@ -5,6 +5,7 @@ import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
@@ -16,10 +17,12 @@ func main() {
// Create application with options
err := wails.Run(&options.App{
Title: "staticanalysis",
Width: 1024,
Height: 768,
Assets: assets,
Title: "staticanalysis",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
+20
View File
@@ -0,0 +1,20 @@
package assetserver
import (
"net/http"
)
// Middleware defines a HTTP middleware that can be applied to the AssetServer.
// The handler passed as next is the next handler in the chain. One can decide to call the next handler
// or implement a specialized handling.
type Middleware func(next http.Handler) http.Handler
// ChainMiddleware allows chaining multiple middlewares to one middleware.
func ChainMiddleware(middleware ...Middleware) Middleware {
return func(h http.Handler) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
h = middleware[i](h)
}
return h
}
}
+35
View File
@@ -0,0 +1,35 @@
package assetserver
import (
"io/fs"
"net/http"
)
// Options defines the configuration of the AssetServer.
type Options struct {
// Assets defines the static assets to be used. A GET request is first tried to be served from this Assets. If the Assets returns
// `os.ErrNotExist` for that file, the request handling will fallback to the Handler and tries to serve the GET
// request from it.
//
// If set to nil, all GET requests will be forwarded to Handler.
Assets fs.FS
// Handler will be called for every GET request that can't be served from Assets, due to `os.ErrNotExist`. Furthermore all
// non GET requests will always be served from this Handler.
//
// If not defined, the result is the following in cases where the Handler would have been called:
// GET request: `http.StatusNotFound`
// Other request: `http.StatusMethodNotAllowed`
Handler http.Handler
// Middleware is a HTTP Middleware which allows to hook into the AssetServer request chain. It allows to skip the default
// request handler dynamically, e.g. implement specialized Routing etc.
// The Middleware is called to build a new `http.Handler` used by the AssetSever and it also receives the default
// handler used by the AssetServer as an argument.
//
// If not defined, the default AssetServer request chain is executed.
//
// Multiple Middlewares can be chained together with:
// ChainMiddleware(middleware ...Middleware) Middleware
Middleware Middleware
}
+8 -3
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"runtime"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/linux"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"github.com/wailsapp/wails/v2/pkg/options/windows"
@@ -47,9 +48,13 @@ type App struct {
AlwaysOnTop bool
// BackgroundColour is the background colour of the window
// You can use the options.NewRGB and options.NewRGBA functions to create a new colour
BackgroundColour *RGBA
Assets fs.FS
AssetsHandler http.Handler
BackgroundColour *RGBA
// Deprecated: Use AssetServer.Assets instead.
Assets fs.FS
// Deprecated: Use AssetServer.Handler instead.
AssetsHandler http.Handler
// AssetServer configures the Assets for the application
AssetServer *assetserver.Options
Menu *menu.Menu
Logger logger.Logger `json:"-"`
LogLevel logger.LogLevel
+7 -4
View File
@@ -5,6 +5,7 @@ import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed frontend/dist
@@ -16,10 +17,12 @@ app := NewApp()
// Create application with options
err := wails.Run(&options.App{
Title: "{{.ProjectName}}",
Width: 1024,
Height: 768,
Assets: assets,
Title: "{{.ProjectName}}",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{
+4 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/windows"
)
@@ -37,7 +38,9 @@ Frameless: false,
StartHidden: false,
HideWindowOnClose: false,
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
Assets: assets,
AssetServer: &assetserver.Options{
Assets: assets,
},
Menu: nil,
Logger: nil,
LogLevel: logger.DEBUG,
@@ -5,6 +5,7 @@ import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
//go:embed all:frontend/dist
@@ -16,10 +17,12 @@ func main() {
// Create application with options
err := wails.Run(&options.App{
Title: "{{.ProjectName}}",
Width: 1024,
Height: 768,
Assets: assets,
Title: "{{.ProjectName}}",
Width: 1024,
Height: 768,
AssetServer: &assetserver.Options{
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
OnStartup: app.startup,
Bind: []interface{}{

Some files were not shown because too many files have changed in this diff Show More