5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 23:20:51 +08:00
wails/v2/pkg/assetserver/assetserver.go
stffabi 496461920f
[v2] DevServer improvements and fixes (#2664)
* [assetserver, darwin] Fix copying request headers by using strdup

* [assetserver, linux] Fake some basic http headers for legacy webkit2 versions to support proxying requests to other servers

This fixes the devserver on v2 for newer vite versions that use the custom
scheme.

* [v2, windows] 304 responses are going to hang the WebView2 so prevent them by removing cache related headers in the request.

* [v2, dev] Now uses the custom schemes `wails://` on macOS and Linux for all Vite versions.

Prevent missing reload after fast multiple savings on Linux and Windows.
2023-05-16 09:35:48 +02:00

232 lines
5.5 KiB
Go

package assetserver
import (
"bytes"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"strings"
"golang.org/x/net/html"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
)
const (
runtimeJSPath = "/wails/runtime.js"
ipcJSPath = "/wails/ipc.js"
runtimePath = "/wails/runtime"
)
type RuntimeAssets interface {
DesktopIPC() []byte
WebsocketIPC() []byte
RuntimeDesktopJS() []byte
}
type RuntimeHandler interface {
HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
}
type AssetServer struct {
handler http.Handler
runtimeJS []byte
ipcJS func(*http.Request) []byte
logger Logger
runtime RuntimeAssets
servingFromDisk bool
appendSpinnerToBody bool
// Use http based runtime
runtimeHandler RuntimeHandler
// plugin scripts
pluginScripts map[string]string
assetServerWebView
}
func NewAssetServerMainPage(bindingsJSON string, options *options.App, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
assetOptions, err := BuildAssetServerConfig(options)
if err != nil {
return nil, err
}
return NewAssetServer(bindingsJSON, assetOptions, servingFromDisk, logger, runtime)
}
func NewAssetServer(bindingsJSON string, options assetserver.Options, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
handler, err := NewAssetHandler(options, logger)
if err != nil {
return nil, err
}
return NewAssetServerWithHandler(handler, bindingsJSON, servingFromDisk, logger, runtime)
}
func NewAssetServerWithHandler(handler http.Handler, bindingsJSON string, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) {
var buffer bytes.Buffer
if bindingsJSON != "" {
buffer.WriteString(`window.wailsbindings='` + bindingsJSON + `';` + "\n")
}
buffer.Write(runtime.RuntimeDesktopJS())
result := &AssetServer{
handler: handler,
runtimeJS: buffer.Bytes(),
// 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.
// We indicate this through the `servingFromDisk` flag to ensure requests
// aren't cached in dev mode.
servingFromDisk: servingFromDisk,
logger: logger,
runtime: runtime,
}
return result, nil
}
func (d *AssetServer) UseRuntimeHandler(handler RuntimeHandler) {
d.runtimeHandler = handler
}
func (d *AssetServer) AddPluginScript(pluginName string, script string) {
if d.pluginScripts == nil {
d.pluginScripts = make(map[string]string)
}
pluginName = strings.ReplaceAll(pluginName, "/", "_")
pluginName = html.EscapeString(pluginName)
pluginScriptName := fmt.Sprintf("/plugin_%s_%d.js", pluginName, rand.Intn(100000))
d.pluginScripts[pluginScriptName] = script
}
func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if isWebSocket(req) {
// WebSockets are not supported by the AssetServer
rw.WriteHeader(http.StatusNotImplemented)
return
}
header := rw.Header()
if d.servingFromDisk {
header.Add(HeaderCacheControl, "no-cache")
}
path := req.URL.Path
switch path {
case "", "/", "/index.html":
recorder := httptest.NewRecorder()
d.handler.ServeHTTP(recorder, req)
for k, v := range recorder.HeaderMap {
header[k] = v
}
switch recorder.Code {
case http.StatusOK:
content, err := d.processIndexHTML(recorder.Body.Bytes())
if err != nil {
d.serveError(rw, err, "Unable to processIndexHTML")
return
}
d.writeBlob(rw, indexHTML, content)
case http.StatusNotFound:
d.writeBlob(rw, indexHTML, defaultHTML)
default:
rw.WriteHeader(recorder.Code)
}
case runtimeJSPath:
d.writeBlob(rw, path, d.runtimeJS)
case runtimePath:
if d.runtimeHandler != nil {
d.runtimeHandler.HandleRuntimeCall(rw, req)
} else {
d.handler.ServeHTTP(rw, req)
}
case ipcJSPath:
content := d.runtime.DesktopIPC()
if d.ipcJS != nil {
content = d.ipcJS(req)
}
d.writeBlob(rw, path, content)
default:
// Check if this is a plugin script
if script, ok := d.pluginScripts[path]; ok {
d.writeBlob(rw, path, []byte(script))
return
}
d.handler.ServeHTTP(rw, req)
}
}
func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
htmlNode, err := getHTMLNode(indexHTML)
if err != nil {
return nil, err
}
if d.appendSpinnerToBody {
err = appendSpinnerToBody(htmlNode)
if err != nil {
return nil, err
}
}
if err := insertScriptInHead(htmlNode, runtimeJSPath); err != nil {
return nil, err
}
if err := insertScriptInHead(htmlNode, ipcJSPath); err != nil {
return nil, err
}
// Inject plugins
for scriptName := range d.pluginScripts {
if err := insertScriptInHead(htmlNode, scriptName); err != nil {
return nil, err
}
}
var buffer bytes.Buffer
err = html.Render(&buffer, htmlNode)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (d *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
err := serveFile(rw, filename, blob)
if err != nil {
d.serveError(rw, err, "Unable to write content %s", filename)
}
}
func (d *AssetServer) serveError(rw http.ResponseWriter, err error, msg string, args ...interface{}) {
args = append(args, err)
d.logError(msg+": %s", args...)
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) logError(message string, args ...interface{}) {
if d.logger != nil {
d.logger.Error("[AssetServer] "+message, args...)
}
}