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:
parent
ee8eb001c2
commit
e6c691a376
@ -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 {
|
||||
|
@ -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...)
|
||||
}
|
||||
|
@ -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...)
|
||||
}
|
||||
|
@ -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
|
||||
//}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user