From e6c691a37622636729e5f5cccfe62b2c41be1cf0 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Mon, 14 Aug 2023 20:49:09 +1000 Subject: [PATCH] Support external asset server --- v2/pkg/assetserver/assetserver_webview.go | 4 + v3/internal/assetserver/assethandler.go | 16 ++-- v3/internal/assetserver/assetserver.go | 73 +++++++++++++++---- v3/internal/assetserver/assetserver_dev.go | 36 ++++----- .../assetserver/assetserver_webview.go | 16 +++- v3/internal/assetserver/options.go | 12 +++ v3/pkg/application/application.go | 9 ++- v3/pkg/application/options_application.go | 4 + v3/pkg/application/webview_window_windows.go | 20 ++++- 9 files changed, 137 insertions(+), 53 deletions(-) diff --git a/v2/pkg/assetserver/assetserver_webview.go b/v2/pkg/assetserver/assetserver_webview.go index 7fad1b01e..fe65acd32 100644 --- a/v2/pkg/assetserver/assetserver_webview.go +++ b/v2/pkg/assetserver/assetserver_webview.go @@ -115,6 +115,10 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) { req.RemoteAddr = "192.0.2.1:1234" } + if req.RequestURI == "" && req.URL != nil { + req.RequestURI = req.URL.String() + } + if req.ContentLength == 0 { req.ContentLength, _ = strconv.ParseInt(req.Header.Get(HeaderContentLength), 10, 64) } else { diff --git a/v3/internal/assetserver/assethandler.go b/v3/internal/assetserver/assethandler.go index 42ca815c5..9b6d11355 100644 --- a/v3/internal/assetserver/assethandler.go +++ b/v3/internal/assetserver/assethandler.go @@ -78,11 +78,11 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if strings.EqualFold(req.Method, http.MethodGet) { 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 os.IsNotExist(err) { 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) err = nil } else { @@ -97,7 +97,7 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } } 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) } else { rw.WriteHeader(http.StatusMethodNotAllowed) @@ -185,14 +185,10 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi return err } -func (d *assetHandler) logDebug(message string, args ...interface{}) { - if d.logger != nil { - d.logger.Debug("[AssetHandler] "+message, args...) - } +func (d *assetHandler) logInfo(message string, args ...interface{}) { + d.logger.Debug("[AssetHandler] "+message, args...) } func (d *assetHandler) logError(message string, args ...interface{}) { - if d.logger != nil { - d.logger.Error("[AssetHandler] "+message, args...) - } + d.logger.Error("[AssetHandler] "+message, args...) } diff --git a/v3/internal/assetserver/assetserver.go b/v3/internal/assetserver/assetserver.go index 150884507..12215f4b1 100644 --- a/v3/internal/assetserver/assetserver.go +++ b/v3/internal/assetserver/assetserver.go @@ -7,6 +7,7 @@ import ( "math/rand" "net/http" "net/http/httptest" + "net/http/httputil" "strings" "golang.org/x/net/html" @@ -38,6 +39,7 @@ type AssetServer struct { logger *slog.Logger runtime RuntimeAssets + options *Options servingFromDisk bool @@ -53,6 +55,9 @@ type AssetServer struct { // GetFlags returns the application flags GetFlags func() []byte + // External dev server proxy + wsHandler *httputil.ReverseProxy + assetServerWebView } @@ -62,10 +67,6 @@ func NewAssetServer(options *Options, servingFromDisk bool, logger *slog.Logger, 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 buffer.Write(runtime.RuntimeDesktopJS()) @@ -73,6 +74,7 @@ func NewAssetServerWithHandler(handler http.Handler, servingFromDisk bool, logge handler: handler, runtimeJS: buffer.Bytes(), runtimeHandler: runtimeHandler, + options: options, // 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. @@ -84,9 +86,44 @@ func NewAssetServerWithHandler(handler http.Handler, servingFromDisk bool, logge 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 } +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) { if d.pluginScripts == nil { d.pluginScripts = make(map[string]string) @@ -97,11 +134,23 @@ func (d *AssetServer) AddPluginScript(pluginName string, script string) { 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) { - if isWebSocket(req) { - // WebSockets are not supported by the AssetServer - rw.WriteHeader(http.StatusNotImplemented) + + 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) { + // WebSockets are not supported by the AssetServer + rw.WriteHeader(http.StatusNotImplemented) + } } header := rw.Header() @@ -223,14 +272,10 @@ func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, rw.WriteHeader(http.StatusInternalServerError) } -func (d *AssetServer) logDebug(message string, args ...interface{}) { - if d.logger != nil { - d.logger.Debug("[AssetServer] "+message, args...) - } +func (d *AssetServer) logInfo(message string, args ...interface{}) { + d.logger.Info("AssetServer: "+message, args...) } func (d *AssetServer) logError(message string, args ...interface{}) { - if d.logger != nil { - d.logger.Error("[AssetServer] "+message, args...) - } + d.logger.Error("AssetServer: "+message, args...) } diff --git a/v3/internal/assetserver/assetserver_dev.go b/v3/internal/assetserver/assetserver_dev.go index 3326bfb82..ae80a74e7 100644 --- a/v3/internal/assetserver/assetserver_dev.go +++ b/v3/internal/assetserver/assetserver_dev.go @@ -2,29 +2,23 @@ package assetserver -import ( - "log/slog" - "net/http" - "strings" -) - /* 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 NewDevAssetServer(handler http.Handler, servingFromDisk bool, logger *slog.Logger, runtime RuntimeAssets, runtimeHandler RuntimeHandler) (*AssetServer, error) { - result, err := NewAssetServerWithHandler(handler, servingFromDisk, logger, runtime, true, runtimeHandler) - if err != nil { - return nil, err - } - - result.ipcJS = func(req *http.Request) []byte { - if strings.Contains(req.UserAgent(), WailsUserAgentValue) { - return runtime.DesktopIPC() - } - return runtime.WebsocketIPC() - } - - return result, nil -} +//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) +// if err != nil { +// return nil, err +// } +// +// result.ipcJS = func(req *http.Request) []byte { +// if strings.Contains(req.UserAgent(), WailsUserAgentValue) { +// return runtime.DesktopIPC() +// } +// return runtime.WebsocketIPC() +// } +// +// return result, nil +//} diff --git a/v3/internal/assetserver/assetserver_webview.go b/v3/internal/assetserver/assetserver_webview.go index 85ee25b70..a039f992d 100644 --- a/v3/internal/assetserver/assetserver_webview.go +++ b/v3/internal/assetserver/assetserver_webview.go @@ -63,7 +63,7 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) { uri, err := r.URL() 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) return } @@ -96,6 +96,18 @@ func (d *AssetServer) processWebViewRequest(r webview.Request) { d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err)) 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 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) } - 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) } diff --git a/v3/internal/assetserver/options.go b/v3/internal/assetserver/options.go index 674451a0c..1ad67cd8c 100644 --- a/v3/internal/assetserver/options.go +++ b/v3/internal/assetserver/options.go @@ -4,6 +4,7 @@ import ( "fmt" "io/fs" "net/http" + "net/url" ) // Options defines the configuration of the AssetServer. @@ -33,6 +34,10 @@ type Options struct { // Multiple Middlewares can be chained together with: // ChainMiddleware(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 @@ -43,3 +48,10 @@ func (o Options) Validate() error { return nil } + +func (o Options) getExternalURL() (*url.URL, error) { + if o.ExternalURL == "" { + return nil, nil + } + return url.Parse(o.ExternalURL) +} diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index d659af40b..998fe1d55 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -54,9 +54,10 @@ func New(appOptions Options) *App { result.Events = NewWailsEventProcessor(result.dispatchEventToWindows) opts := &assetserver.Options{ - Assets: appOptions.Assets.FS, - Handler: appOptions.Assets.Handler, - Middleware: assetserver.Middleware(appOptions.Assets.Middleware), + Assets: appOptions.Assets.FS, + Handler: appOptions.Assets.Handler, + Middleware: assetserver.Middleware(appOptions.Assets.Middleware), + ExternalURL: appOptions.Assets.ExternalURL, } 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 { o.Icon = icons.ApplicationLightMode256 } - } type ( @@ -388,6 +388,7 @@ func (a *App) NewSystemTray() *SystemTray { func (a *App) Run() error { a.logStartup() a.logPlatformInfo() + a.assets.LogDetails() // Setup panic handler defer processPanicHandlerRecover() diff --git a/v3/pkg/application/options_application.go b/v3/pkg/application/options_application.go index 32a152798..bb07c39a5 100644 --- a/v3/pkg/application/options_application.go +++ b/v3/pkg/application/options_application.go @@ -62,6 +62,10 @@ type AssetOptions struct { // Multiple Middlewares can be chained together with: // ChainMiddleware(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. diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 938f1f454..f6fe1f682 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -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() if err != nil { 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, ","))) } + 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() - 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 { globalApplication.error("CreateWebResourceResponse Error: " + err.Error()) return @@ -1447,7 +1461,9 @@ func (w *windowsWebviewWindow) setupChromium() { chromium.NavigateToString(w.parent.options.HTML) } else { 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 parsedURL, err := url.Parse(w.parent.options.URL) if err != nil {