5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-14 16:09:31 +08:00

Support external asset server

This commit is contained in:
Lea Anthony 2023-08-14 20:49:09 +10:00
parent ee8eb001c2
commit e6c691a376
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
9 changed files with 137 additions and 53 deletions

View File

@ -115,6 +115,10 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) {
req.RemoteAddr = "192.0.2.1:1234" req.RemoteAddr = "192.0.2.1:1234"
} }
if req.RequestURI == "" && req.URL != nil {
req.RequestURI = req.URL.String()
}
if req.ContentLength == 0 { if req.ContentLength == 0 {
req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64) req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64)
} else { } else {

View File

@ -78,11 +78,11 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if strings.EqualFold(req.Method, http.MethodGet) { if strings.EqualFold(req.Method, http.MethodGet) {
filename := path.Clean(strings.TrimPrefix(url, "/")) filename := path.Clean(strings.TrimPrefix(url, "/"))
d.logDebug("Handling request '%s' (file='%s')", url, filename) d.logInfo("Handling request", "url", url, "file", filename)
if err := d.serveFSFile(rw, req, filename); err != nil { if err := d.serveFSFile(rw, req, filename); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if handler != nil { if handler != nil {
d.logDebug("File '%s' not found, serving '%s' by AssetHandler", filename, url) d.logInfo("File not found. Deferring to AssetHandler", "filename", filename, "url", url)
handler.ServeHTTP(rw, req) handler.ServeHTTP(rw, req)
err = nil err = nil
} else { } else {
@ -97,7 +97,7 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
} }
} }
} else if handler != nil { } else if handler != nil {
d.logDebug("No GET request, serving '%s' by AssetHandler", url) d.logInfo("Non-GET request. Deferring to AssetHandler", "url", url)
handler.ServeHTTP(rw, req) handler.ServeHTTP(rw, req)
} else { } else {
rw.WriteHeader(http.StatusMethodNotAllowed) rw.WriteHeader(http.StatusMethodNotAllowed)
@ -185,14 +185,10 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi
return err return err
} }
func (d *assetHandler) logDebug(message string, args ...interface{}) { func (d *assetHandler) logInfo(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Debug("[AssetHandler] "+message, args...) d.logger.Debug("[AssetHandler] "+message, args...)
} }
}
func (d *assetHandler) logError(message string, args ...interface{}) { func (d *assetHandler) logError(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Error("[AssetHandler] "+message, args...) d.logger.Error("[AssetHandler] "+message, args...)
} }
}

View File

@ -7,6 +7,7 @@ import (
"math/rand" "math/rand"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/http/httputil"
"strings" "strings"
"golang.org/x/net/html" "golang.org/x/net/html"
@ -38,6 +39,7 @@ type AssetServer struct {
logger *slog.Logger logger *slog.Logger
runtime RuntimeAssets runtime RuntimeAssets
options *Options
servingFromDisk bool servingFromDisk bool
@ -53,6 +55,9 @@ type AssetServer struct {
// GetFlags returns the application flags // GetFlags returns the application flags
GetFlags func() []byte GetFlags func() []byte
// External dev server proxy
wsHandler *httputil.ReverseProxy
assetServerWebView assetServerWebView
} }
@ -62,10 +67,6 @@ func NewAssetServer(options *Options, servingFromDisk bool, logger *slog.Logger,
return nil, err return nil, err
} }
return NewAssetServerWithHandler(handler, servingFromDisk, logger, runtime, debug, runtimeHandler)
}
func NewAssetServerWithHandler(handler http.Handler, servingFromDisk bool, logger *slog.Logger, runtime RuntimeAssets, debug bool, runtimeHandler RuntimeHandler) (*AssetServer, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
buffer.Write(runtime.RuntimeDesktopJS()) buffer.Write(runtime.RuntimeDesktopJS())
@ -73,6 +74,7 @@ func NewAssetServerWithHandler(handler http.Handler, servingFromDisk bool, logge
handler: handler, handler: handler,
runtimeJS: buffer.Bytes(), runtimeJS: buffer.Bytes(),
runtimeHandler: runtimeHandler, runtimeHandler: runtimeHandler,
options: options,
// Check if we have been given a directory to serve assets from. // Check if we have been given a directory to serve assets from.
// If so, this means we are in dev mode and are serving assets off disk. // If so, this means we are in dev mode and are serving assets off disk.
@ -84,9 +86,44 @@ func NewAssetServerWithHandler(handler http.Handler, servingFromDisk bool, logge
debug: debug, debug: debug,
} }
// Check if proxy required
externalURL, err := options.getExternalURL()
if err != nil {
return nil, err
} else {
result.wsHandler = httputil.NewSingleHostReverseProxy(externalURL)
err := result.checkExternalURL()
if err != nil {
return nil, err
}
}
return result, nil return result, nil
} }
func (d *AssetServer) checkExternalURL() error {
req, err := http.NewRequest("OPTIONS", "/", nil)
if err != nil {
return err
}
w := httptest.NewRecorder()
d.wsHandler.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
return fmt.Errorf("unable to connect to external server: %s. Please check it's running.", d.options.ExternalURL)
}
return nil
}
func (d *AssetServer) LogDetails() {
if d.debug {
d.logger.Info("AssetServer Info:",
"assetsFS", d.options.Assets != nil,
"middleware", d.options.Middleware != nil,
"handler", d.options.Handler != nil,
"externalURL", d.options.ExternalURL,
)
}
}
func (d *AssetServer) AddPluginScript(pluginName string, script string) { func (d *AssetServer) AddPluginScript(pluginName string, script string) {
if d.pluginScripts == nil { if d.pluginScripts == nil {
d.pluginScripts = make(map[string]string) d.pluginScripts = make(map[string]string)
@ -97,11 +134,23 @@ func (d *AssetServer) AddPluginScript(pluginName string, script string) {
d.pluginScripts[pluginScriptName] = script d.pluginScripts[pluginScriptName] = script
} }
func (d *AssetServer) logRequest(req *http.Request, code int) {
d.logger.Info("AssetServer:", "code", code, "method", req.Method, "url", req.URL.Path)
}
func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if d.wsHandler != nil {
d.wsHandler.ServeHTTP(rw, req)
// Get response code from header
code := rw.(*contentTypeSniffer).rw.(*legacyRequestNoOpCloserResponseWriter).ResponseWriter.(*httptest.ResponseRecorder).Code
d.logRequest(req, code)
return
} else {
if isWebSocket(req) { if isWebSocket(req) {
// WebSockets are not supported by the AssetServer // WebSockets are not supported by the AssetServer
rw.WriteHeader(http.StatusNotImplemented) rw.WriteHeader(http.StatusNotImplemented)
return }
} }
header := rw.Header() header := rw.Header()
@ -223,14 +272,10 @@ func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string,
rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusInternalServerError)
} }
func (d *AssetServer) logDebug(message string, args ...interface{}) { func (d *AssetServer) logInfo(message string, args ...interface{}) {
if d.logger != nil { d.logger.Info("AssetServer: "+message, args...)
d.logger.Debug("[AssetServer] "+message, args...)
}
} }
func (d *AssetServer) logError(message string, args ...interface{}) { func (d *AssetServer) logError(message string, args ...interface{}) {
if d.logger != nil { d.logger.Error("AssetServer: "+message, args...)
d.logger.Error("[AssetServer] "+message, args...)
}
} }

View File

@ -2,29 +2,23 @@
package assetserver package assetserver
import (
"log/slog"
"net/http"
"strings"
)
/* /*
The assetserver for the dev mode. 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 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. default desktop IPC is injected when the webview accesses the devserver.
*/ */
func NewDevAssetServer(handler http.Handler, servingFromDisk bool, logger *slog.Logger, runtime RuntimeAssets, runtimeHandler RuntimeHandler) (*AssetServer, error) { //func NewDevAssetServer(handler http.Handler, servingFromDisk bool, logger *slog.Logger, runtime RuntimeAssets, runtimeHandler RuntimeHandler) (*AssetServer, error) {
result, err := NewAssetServerWithHandler(handler, servingFromDisk, logger, runtime, true, runtimeHandler) // result, err := NewAssetServerWithHandler(handler, servingFromDisk, logger, runtime, true, runtimeHandler)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
//
result.ipcJS = func(req *http.Request) []byte { // result.ipcJS = func(req *http.Request) []byte {
if strings.Contains(req.UserAgent(), WailsUserAgentValue) { // if strings.Contains(req.UserAgent(), WailsUserAgentValue) {
return runtime.DesktopIPC() // return runtime.DesktopIPC()
} // }
return runtime.WebsocketIPC() // return runtime.WebsocketIPC()
} // }
//
return result, nil // return result, nil
} //}

View File

@ -63,7 +63,7 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) {
uri, err := r.URL() uri, err := r.URL()
if err != nil { if err != nil {
d.logError("Error processing request, unable to get URL: %s (HttpResponse=500)", err) d.logError("Error processing request, unable to get URL (HttpResponse=500)", "error", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
return return
} }
@ -96,6 +96,18 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) {
d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err)) d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err))
return return
} }
// For server requests, the URL is parsed from the URI supplied on the Request-Line as stored in RequestURI. For
// most requests, fields other than Path and RawQuery will be empty. (See RFC 7230, Section 5.3)
req.URL.Scheme = ""
req.URL.Host = ""
req.URL.Fragment = ""
req.URL.RawFragment = ""
if url := req.URL; req.RequestURI == "" && url != nil {
req.RequestURI = url.String()
}
req.Header = header req.Header = header
if req.RemoteAddr == "" { if req.RemoteAddr == "" {
@ -131,7 +143,7 @@ func (d *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWri
logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1) logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1)
} }
d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err) d.logError("Error processing request (HttpResponse=500)", "details", logInfo, "error", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"net/url"
) )
// Options defines the configuration of the AssetServer. // Options defines the configuration of the AssetServer.
@ -33,6 +34,10 @@ type Options struct {
// Multiple Middlewares can be chained together with: // Multiple Middlewares can be chained together with:
// ChainMiddleware(middleware ...Middleware) Middleware // ChainMiddleware(middleware ...Middleware) Middleware
Middleware Middleware Middleware Middleware
// ExternalURL is the URL that the assets are served from
// This is useful when using a development server like `vite` or `snowpack` which serves the assets on a different port.
ExternalURL string
} }
// Validate the options // Validate the options
@ -43,3 +48,10 @@ func (o Options) Validate() error {
return nil return nil
} }
func (o Options) getExternalURL() (*url.URL, error) {
if o.ExternalURL == "" {
return nil, nil
}
return url.Parse(o.ExternalURL)
}

View File

@ -57,6 +57,7 @@ func New(appOptions Options) *App {
Assets: appOptions.Assets.FS, Assets: appOptions.Assets.FS,
Handler: appOptions.Assets.Handler, Handler: appOptions.Assets.Handler,
Middleware: assetserver.Middleware(appOptions.Assets.Middleware), Middleware: assetserver.Middleware(appOptions.Assets.Middleware),
ExternalURL: appOptions.Assets.ExternalURL,
} }
srv, err := assetserver.NewAssetServer(opts, false, result.Logger, wailsruntime.RuntimeAssetsBundle, result.isDebugMode, NewMessageProcessor()) srv, err := assetserver.NewAssetServer(opts, false, result.Logger, wailsruntime.RuntimeAssetsBundle, result.isDebugMode, NewMessageProcessor())
@ -112,7 +113,6 @@ func mergeApplicationDefaults(o *Options) {
if o.Icon == nil { if o.Icon == nil {
o.Icon = icons.ApplicationLightMode256 o.Icon = icons.ApplicationLightMode256
} }
} }
type ( type (
@ -388,6 +388,7 @@ func (a *App) NewSystemTray() *SystemTray {
func (a *App) Run() error { func (a *App) Run() error {
a.logStartup() a.logStartup()
a.logPlatformInfo() a.logPlatformInfo()
a.assets.LogDetails()
// Setup panic handler // Setup panic handler
defer processPanicHandlerRecover() defer processPanicHandlerRecover()

View File

@ -62,6 +62,10 @@ type AssetOptions struct {
// Multiple Middlewares can be chained together with: // Multiple Middlewares can be chained together with:
// ChainMiddleware(middleware ...Middleware) Middleware // ChainMiddleware(middleware ...Middleware) Middleware
Middleware Middleware Middleware Middleware
// External URL can be set to a development server URL so that all requests are forwarded to it. This is useful
// when using a development server like `vite` or `snowpack` which serves the assets on a different port.
ExternalURL string
} }
// Middleware defines a HTTP middleware that can be applied to the AssetServer. // Middleware defines a HTTP middleware that can be applied to the AssetServer.

View File

@ -1238,6 +1238,12 @@ func coreWebview2RequestToHttpRequest(coreReq *edge.ICoreWebView2WebResourceRequ
} }
} }
// WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
// requests including IPC calls.
// So prevent 304 status codes by removing the headers that are used in combination with caching.
header.Del("If-Modified-Since")
header.Del("If-None-Match")
method, err := coreReq.GetMethod() method, err := coreReq.GetMethod()
if err != nil { if err != nil {
return nil, fmt.Errorf("GetMethod Error: %s", err) return nil, fmt.Errorf("GetMethod Error: %s", err)
@ -1334,8 +1340,16 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource
headers = append(headers, fmt.Sprintf("%s: %s", k, strings.Join(v, ","))) headers = append(headers, fmt.Sprintf("%s: %s", k, strings.Join(v, ",")))
} }
code := rw.Code
if code == http.StatusNotModified {
// WebView2 has problems when a request returns a 304 status code and the WebView2 is going to hang for other
// requests including IPC calls.
globalApplication.error("AssetServer returned 304 - StatusNotModified which is going to hang WebView2, changed code to 505 - StatusInternalServerError", "uri", uri)
code = http.StatusInternalServerError
}
env := w.chromium.Environment() env := w.chromium.Environment()
response, err := env.CreateWebResourceResponse(rw.Body.Bytes(), rw.Code, http.StatusText(rw.Code), strings.Join(headers, "\n")) response, err := env.CreateWebResourceResponse(rw.Body.Bytes(), code, http.StatusText(code), strings.Join(headers, "\n"))
if err != nil { if err != nil {
globalApplication.error("CreateWebResourceResponse Error: " + err.Error()) globalApplication.error("CreateWebResourceResponse Error: " + err.Error())
return return
@ -1447,7 +1461,9 @@ func (w *windowsWebviewWindow) setupChromium() {
chromium.NavigateToString(w.parent.options.HTML) chromium.NavigateToString(w.parent.options.HTML)
} else { } else {
var startURL = "http://wails.localhost" var startURL = "http://wails.localhost"
if w.parent.options.URL != "" { if globalApplication.options.Assets.ExternalURL != "" {
startURL = globalApplication.options.Assets.ExternalURL
} else if w.parent.options.URL != "" {
// parse the url // parse the url
parsedURL, err := url.Parse(w.parent.options.URL) parsedURL, err := url.Parse(w.parent.options.URL)
if err != nil { if err != nil {