5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-12 06:59:30 +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"
}
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 {

View File

@ -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...)
}

View File

@ -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...)
}

View File

@ -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
//}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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.

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()
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 {