mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 17:52:29 +08:00
[v2] Add support for AssetsHandler (#1325)
* [website] Fix devserver default value doc * [v2] Add support for AssetsHandler AssetsHandler is a http.Handler delegate, which gets called as a fallback for all Non-GET requests and for GET requests for which the Assets didn’t find the file. Known Limitations on Linux: - All requests are GET requests - No request headers - No request body - No response status code, only StatusOK will be returned - No response headers Known Limitations on Windows: - Request body is leaking memory. Seems to be a bug in WebView2, investigation angoing. Most of these limitations on Linux will be fixed in the future with adding support for Webkit2Gtk 2.36.0+. * [v2, linux] Add response streaming support The complete response won’t be held anymore in memory and will be streamed to WebKit2. Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
parent
3fbe4f71c4
commit
6d09a45a30
@ -9,6 +9,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
iofs "io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@ -114,7 +115,13 @@ func CreateApp(appoptions *options.App) (*App, error) {
|
||||
if devServer == "" {
|
||||
return nil, fmt.Errorf("Unable to use FrontendDevServerUrl without a DevServer address")
|
||||
}
|
||||
ctx = context.WithValue(ctx, "starturl", "http://"+devServer)
|
||||
|
||||
startURL, err := url.Parse("http://" + devServer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, "starturl", startURL)
|
||||
ctx = context.WithValue(ctx, "frontenddevserverurl", frontendDevServerURL)
|
||||
|
||||
myLogger.Info("Serving assets from frontend DevServer URL: %s", frontendDevServerURL)
|
||||
|
@ -1,8 +1,11 @@
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
iofs "io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -18,8 +21,13 @@ import (
|
||||
//go:embed defaultindex.html
|
||||
var defaultHTML []byte
|
||||
|
||||
const (
|
||||
indexHTML = "index.html"
|
||||
)
|
||||
|
||||
type assetHandler struct {
|
||||
fs iofs.FS
|
||||
fs iofs.FS
|
||||
handler http.Handler
|
||||
|
||||
logger *logger.Logger
|
||||
|
||||
@ -33,7 +41,7 @@ func NewAsssetHandler(ctx context.Context, options *options.App) (http.Handler,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subDir, err := fs.FindPathToFile(vfs, "index.html")
|
||||
subDir, err := fs.FindPathToFile(vfs, indexHTML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -45,7 +53,8 @@ func NewAsssetHandler(ctx context.Context, options *options.App) (http.Handler,
|
||||
}
|
||||
|
||||
result := &assetHandler{
|
||||
fs: vfs,
|
||||
fs: vfs,
|
||||
handler: options.AssetsHandler,
|
||||
|
||||
// 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.
|
||||
@ -62,63 +71,96 @@ func NewAsssetHandler(ctx context.Context, options *options.App) (http.Handler,
|
||||
}
|
||||
|
||||
func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if d.fs == nil {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
filename := strings.TrimPrefix(req.URL.Path, "/")
|
||||
if d.logger != nil {
|
||||
d.logger.Debug("[AssetHandler] Loading file '%s'", filename)
|
||||
}
|
||||
|
||||
var content []byte
|
||||
var err error
|
||||
switch filename {
|
||||
case "", "index.html":
|
||||
content, err = d.loadFile("index.html")
|
||||
if err != nil {
|
||||
err = nil
|
||||
content = defaultHTML
|
||||
handler := d.handler
|
||||
if strings.EqualFold(req.Method, http.MethodGet) {
|
||||
filename := strings.TrimPrefix(req.URL.Path, "/")
|
||||
if filename == "" {
|
||||
filename = indexHTML
|
||||
}
|
||||
|
||||
default:
|
||||
content, err = d.loadFile(filename)
|
||||
d.logDebug("[AssetHandler] Loading file '%s'", filename)
|
||||
if err := d.serveFSFile(rw, filename); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if handler != nil {
|
||||
d.logDebug("[AssetHandler] File '%s' not found, serving '%s' by AssetHandler", filename, req.URL)
|
||||
handler.ServeHTTP(rw, req)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
d.logError("[AssetHandler] Unable to load file '%s': %s", filename, err)
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
} else if handler != nil {
|
||||
d.logDebug("[AssetHandler] No GET request, serving '%s' by AssetHandler", req.URL)
|
||||
handler.ServeHTTP(rw, req)
|
||||
} else {
|
||||
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// serveFile will try to load the file from the fs.FS and write it to the response
|
||||
func (d *assetHandler) serveFSFile(rw http.ResponseWriter, filename string) error {
|
||||
if d.fs == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
mimeType := GetMimetype(filename, content)
|
||||
rw.Header().Set(HeaderContentType, mimeType)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, err = rw.Write(content)
|
||||
file, err := d.fs.Open(filename)
|
||||
if err != nil && d.servingFromDisk {
|
||||
for tries := 0; tries < 50; tries++ {
|
||||
file, err = d.fs.Open(filename)
|
||||
if err != nil {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
if d.logger != nil {
|
||||
d.logger.Error("[AssetHandler] Unable to load file '%s': %s", filename, err)
|
||||
if filename == indexHTML && os.IsNotExist(err) {
|
||||
return serveFile(rw, filename, defaultHTML)
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
statInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rw.Header().Set(HeaderContentLength, fmt.Sprintf("%d", statInfo.Size()))
|
||||
|
||||
var buf [512]byte
|
||||
n, err := file.Read(buf[:])
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
// Detect MimeType by sniffing the first 512 bytes
|
||||
if contentType := GetMimetype(filename, buf[:n]); contentType != "" {
|
||||
rw.Header().Set(HeaderContentType, contentType)
|
||||
}
|
||||
|
||||
// Write the first bytes
|
||||
_, err = io.Copy(rw, bytes.NewReader(buf[:n]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the remaining content of the file
|
||||
_, err = io.Copy(rw, file)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *assetHandler) logDebug(message string, args ...interface{}) {
|
||||
if d.logger != nil {
|
||||
d.logger.Debug("[AssetHandler] "+message, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// loadFile will try to load the file from disk. If there is an error
|
||||
// it will retry until eventually it will give up and error.
|
||||
func (d *assetHandler) loadFile(filename string) ([]byte, error) {
|
||||
if !d.servingFromDisk {
|
||||
return iofs.ReadFile(d.fs, filename)
|
||||
func (d *assetHandler) logError(message string, args ...interface{}) {
|
||||
if d.logger != nil {
|
||||
d.logger.Error("[AssetHandler] "+message, args...)
|
||||
}
|
||||
var result []byte
|
||||
var err error
|
||||
for tries := 0; tries < 50; tries++ {
|
||||
result, err = iofs.ReadFile(d.fs, filename)
|
||||
if err != nil {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
@ -106,26 +106,45 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AssetServer) Load(filename string) ([]byte, string, error) {
|
||||
// This will be removed as soon as AssetsHandler have been fully introduced.
|
||||
if !strings.HasPrefix(filename, "/") {
|
||||
filename = "/" + filename
|
||||
}
|
||||
// ProcessHTTPRequest processes the HTTP Request by faking a golang HTTP Server.
|
||||
// The request will be finished with a StatusNotImplemented code if no handler has written to the response.
|
||||
func (d *AssetServer) ProcessHTTPRequest(logInfo string, rw http.ResponseWriter, reqGetter func() (*http.Request, error)) {
|
||||
rw = &contentTypeSniffer{rw: rw} // Make sure we have a Content-Type sniffer
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "wails://wails"+filename, nil)
|
||||
req, err := reqGetter()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err)
|
||||
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Body == nil {
|
||||
req.Body = http.NoBody
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
if req.RemoteAddr == "" {
|
||||
// 192.0.2.0/24 is "TEST-NET" in RFC 5737
|
||||
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 {
|
||||
req.Header.Set(HeaderContentLength, fmt.Sprintf("%d", req.ContentLength))
|
||||
}
|
||||
|
||||
if host := req.Header.Get(HeaderHost); host != "" {
|
||||
req.Host = host
|
||||
}
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
d.ServeHTTP(rw, req)
|
||||
|
||||
content := rw.Body.Bytes()
|
||||
mimeType := rw.HeaderMap.Get(HeaderContentType)
|
||||
if mimeType == "" {
|
||||
mimeType = GetMimetype(filename, content)
|
||||
}
|
||||
return content, mimeType, nil
|
||||
rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status
|
||||
}
|
||||
|
||||
func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
|
||||
@ -158,15 +177,8 @@ func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (d *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {
|
||||
header := rw.Header()
|
||||
header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob)))
|
||||
if mimeType := header.Get(HeaderContentType); mimeType == "" {
|
||||
mimeType = GetMimetype(filename, blob)
|
||||
header.Set(HeaderContentType, mimeType)
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
if _, err := rw.Write(blob); err != nil {
|
||||
err := serveFile(rw, filename, blob)
|
||||
if err != nil {
|
||||
d.serveError(rw, err, "Unable to write content %s", filename)
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,15 @@ package assetserver
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderHost = "Host"
|
||||
HeaderContentType = "Content-Type"
|
||||
HeaderContentLength = "Content-Length"
|
||||
HeaderUserAgent = "User-Agent"
|
||||
@ -16,6 +20,19 @@ const (
|
||||
WailsUserAgentValue = "wails.io"
|
||||
)
|
||||
|
||||
func serveFile(rw http.ResponseWriter, filename string, blob []byte) error {
|
||||
header := rw.Header()
|
||||
header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob)))
|
||||
if mimeType := header.Get(HeaderContentType); mimeType == "" {
|
||||
mimeType = GetMimetype(filename, blob)
|
||||
header.Set(HeaderContentType, mimeType)
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, err := io.Copy(rw, bytes.NewReader(blob))
|
||||
return err
|
||||
}
|
||||
|
||||
func createScriptNode(scriptName string) *html.Node {
|
||||
return &html.Node{
|
||||
Type: html.ElementNode,
|
||||
|
42
v2/internal/frontend/assetserver/content_type_sniffer.go
Normal file
42
v2/internal/frontend/assetserver/content_type_sniffer.go
Normal file
@ -0,0 +1,42 @@
|
||||
package assetserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type contentTypeSniffer struct {
|
||||
rw http.ResponseWriter
|
||||
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
func (rw *contentTypeSniffer) Header() http.Header {
|
||||
return rw.rw.Header()
|
||||
}
|
||||
|
||||
func (rw *contentTypeSniffer) Write(buf []byte) (int, error) {
|
||||
rw.writeHeader(buf)
|
||||
return rw.rw.Write(buf)
|
||||
}
|
||||
|
||||
func (rw *contentTypeSniffer) WriteHeader(code int) {
|
||||
if rw.wroteHeader {
|
||||
return
|
||||
}
|
||||
|
||||
rw.rw.WriteHeader(code)
|
||||
rw.wroteHeader = true
|
||||
}
|
||||
|
||||
func (rw *contentTypeSniffer) writeHeader(b []byte) {
|
||||
if rw.wroteHeader {
|
||||
return
|
||||
}
|
||||
|
||||
m := rw.rw.Header()
|
||||
if _, hasType := m[HeaderContentType]; !hasType {
|
||||
m.Set(HeaderContentType, http.DetectContentType(b))
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
|
||||
)
|
||||
|
||||
type RequestRespone struct {
|
||||
Body []byte
|
||||
MimeType string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (r RequestRespone) StatusText() string {
|
||||
return http.StatusText(r.StatusCode)
|
||||
}
|
||||
|
||||
func (r RequestRespone) String() string {
|
||||
return fmt.Sprintf("Body: '%s', StatusCode: %d", string(r.Body), r.StatusCode)
|
||||
}
|
||||
|
||||
func ProcessRequest(uri string, assets *assetserver.AssetServer, expectedScheme string, expectedHosts ...string) (RequestRespone, error) {
|
||||
// Translate URI to file
|
||||
file, err := translateUriToFile(uri, expectedScheme, expectedHosts...)
|
||||
if err != nil {
|
||||
if err == ErrUnexpectedHost {
|
||||
body := fmt.Sprintf("expected host one of \"%s\"", strings.Join(expectedHosts, ","))
|
||||
return textResponse(body, http.StatusInternalServerError), err
|
||||
}
|
||||
|
||||
return RequestRespone{StatusCode: http.StatusInternalServerError}, err
|
||||
}
|
||||
|
||||
content, mimeType, err := assets.Load(file)
|
||||
if err != nil {
|
||||
statusCode := http.StatusInternalServerError
|
||||
if os.IsNotExist(err) {
|
||||
statusCode = http.StatusNotFound
|
||||
}
|
||||
return RequestRespone{StatusCode: statusCode}, err
|
||||
}
|
||||
|
||||
return RequestRespone{Body: content, MimeType: mimeType, StatusCode: http.StatusOK}, nil
|
||||
}
|
||||
|
||||
func textResponse(body string, statusCode int) RequestRespone {
|
||||
if body == "" {
|
||||
return RequestRespone{StatusCode: statusCode}
|
||||
}
|
||||
return RequestRespone{Body: []byte(body), MimeType: "text/plain;charset=UTF-8", StatusCode: statusCode}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var ErrUnexpectedScheme = fmt.Errorf("unexpected scheme")
|
||||
var ErrUnexpectedHost = fmt.Errorf("unexpected host")
|
||||
|
||||
func translateUriToFile(uri string, expectedScheme string, expectedHosts ...string) (file string, err error) {
|
||||
url, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if url.Scheme != expectedScheme {
|
||||
return "", ErrUnexpectedScheme
|
||||
}
|
||||
|
||||
for _, expectedHost := range expectedHosts {
|
||||
if url.Host != expectedHost {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := url.Path
|
||||
if filePath == "" {
|
||||
filePath = "/"
|
||||
}
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
return "", ErrUnexpectedHost
|
||||
}
|
@ -42,7 +42,7 @@ void Quit(void*);
|
||||
const char* GetSize(void *ctx);
|
||||
const char* GetPosition(void *ctx);
|
||||
|
||||
void ProcessURLResponse(void *inctx, const char *url, int statusCode, const char *contentType, void* data, int datalength);
|
||||
void ProcessURLResponse(void *inctx, const char *url, int statusCode, void *headersString, int headersStringLength, void* data, int datalength);
|
||||
|
||||
/* Dialogs */
|
||||
|
||||
|
@ -51,15 +51,16 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
|
||||
return result;
|
||||
}
|
||||
|
||||
void ProcessURLResponse(void *inctx, const char *url, int statusCode, const char *contentType, void* data, int datalength) {
|
||||
void ProcessURLResponse(void *inctx, const char *url, int statusCode, void *headersString, int headersStringLength, void* data, int datalength) {
|
||||
WailsContext *ctx = (__bridge WailsContext*) inctx;
|
||||
NSString *nsurl = safeInit(url);
|
||||
NSString *nsContentType = safeInit(contentType);
|
||||
NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength];
|
||||
NSData *nsdata = [NSData dataWithBytes:data length:datalength];
|
||||
|
||||
[ctx processURLResponse:nsurl :statusCode :nsContentType :nsdata];
|
||||
[ctx processURLResponse:nsurl :statusCode :nsHeadersJSON :nsdata];
|
||||
|
||||
[nsdata release];
|
||||
[nsHeadersJSON release];
|
||||
}
|
||||
|
||||
void ExecJS(void* inctx, const char *script) {
|
||||
|
@ -81,7 +81,7 @@
|
||||
- (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters;
|
||||
|
||||
- (void) loadRequest:(NSString*)url;
|
||||
- (void) processURLResponse:(NSString *)url :(int)statusCode :(NSString *)contentType :(NSData*)data;
|
||||
- (void) processURLResponse:(NSString *)url :(int)statusCode :(NSData *)headersString :(NSData*)data;
|
||||
- (void) ExecJS:(NSString*)script;
|
||||
- (NSScreen*) getCurrentScreen;
|
||||
|
||||
|
@ -376,26 +376,44 @@
|
||||
[self.webview evaluateJavaScript:script completionHandler:nil];
|
||||
}
|
||||
|
||||
- (void) processURLResponse:(NSString *)url :(int)statusCode :(NSString *)contentType :(NSData *)data {
|
||||
- (void) processURLResponse:(NSString *)url :(int)statusCode :(NSData *)headersJSON :(NSData *)data {
|
||||
id<WKURLSchemeTask> urlSchemeTask = self.urlRequests[url];
|
||||
NSURL *nsurl = [NSURL URLWithString:url];
|
||||
NSMutableDictionary *headerFields = [NSMutableDictionary new];
|
||||
if ( ![contentType isEqualToString:@""] ) {
|
||||
headerFields[@"content-type"] = contentType;
|
||||
}
|
||||
NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData: headersJSON options: NSJSONReadingMutableContainers error: nil];
|
||||
NSHTTPURLResponse *response = [[NSHTTPURLResponse new] initWithURL:nsurl statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields];
|
||||
[urlSchemeTask didReceiveResponse:response];
|
||||
[urlSchemeTask didReceiveData:data];
|
||||
[urlSchemeTask didFinish];
|
||||
[self.urlRequests removeObjectForKey:url];
|
||||
[response release];
|
||||
[headerFields release];
|
||||
if (headerFields != nil) {
|
||||
[headerFields release];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
|
||||
// Do something
|
||||
// This callback is run with an autorelease pool
|
||||
self.urlRequests[urlSchemeTask.request.URL.absoluteString] = urlSchemeTask;
|
||||
processURLRequest(self, [urlSchemeTask.request.URL.absoluteString UTF8String]);
|
||||
const char *url = [urlSchemeTask.request.URL.absoluteString UTF8String];
|
||||
const char *method = [urlSchemeTask.request.HTTPMethod UTF8String];
|
||||
const char *headerJSON = "";
|
||||
const void *body;
|
||||
int bodyLen;
|
||||
|
||||
NSData *headers = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil];
|
||||
if (headers) {
|
||||
NSString* headerString = [[[NSString alloc] initWithData:headers encoding:NSUTF8StringEncoding] autorelease];
|
||||
headerJSON = [headerString UTF8String];
|
||||
}
|
||||
|
||||
if (urlSchemeTask.request.HTTPBody) {
|
||||
body = urlSchemeTask.request.HTTPBody.bytes;
|
||||
bodyLen = urlSchemeTask.request.HTTPBody.length;
|
||||
} else {
|
||||
// TODO handle HTTPBodyStream
|
||||
}
|
||||
|
||||
processURLRequest(self, url, method, headerJSON, body, bodyLen);
|
||||
}
|
||||
|
||||
- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id<WKURLSchemeTask>)urlSchemeTask {
|
||||
|
@ -14,26 +14,26 @@ package darwin
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/common"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
|
||||
type request struct {
|
||||
url *C.char
|
||||
ctx unsafe.Pointer
|
||||
}
|
||||
|
||||
var messageBuffer = make(chan string, 100)
|
||||
var requestBuffer = make(chan *request, 100)
|
||||
var callbackBuffer = make(chan uint, 10)
|
||||
@ -49,39 +49,27 @@ type Frontend struct {
|
||||
|
||||
// Assets
|
||||
assets *assetserver.AssetServer
|
||||
startURL string
|
||||
startURL *url.URL
|
||||
|
||||
// main window handle
|
||||
mainWindow *Window
|
||||
bindings *binding.Bindings
|
||||
dispatcher frontend.Dispatcher
|
||||
servingFromDisk bool
|
||||
mainWindow *Window
|
||||
bindings *binding.Bindings
|
||||
dispatcher frontend.Dispatcher
|
||||
}
|
||||
|
||||
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
|
||||
|
||||
result := &Frontend{
|
||||
frontendOptions: appoptions,
|
||||
logger: myLogger,
|
||||
bindings: appBindings,
|
||||
dispatcher: dispatcher,
|
||||
ctx: ctx,
|
||||
startURL: "wails://wails/",
|
||||
}
|
||||
result.startURL, _ = url.Parse("wails://wails/")
|
||||
|
||||
_starturl, _ := ctx.Value("starturl").(string)
|
||||
if _starturl != "" {
|
||||
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
|
||||
result.startURL = _starturl
|
||||
} else {
|
||||
// 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 by WebView2 in dev mode
|
||||
_assetdir := ctx.Value("assetdir")
|
||||
if _assetdir != nil {
|
||||
result.servingFromDisk = true
|
||||
}
|
||||
|
||||
bindingsJSON, err := appBindings.ToJSON()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@ -155,7 +143,7 @@ func (f *Frontend) Run(ctx context.Context) error {
|
||||
f.frontendOptions.OnStartup(f.ctx)
|
||||
}
|
||||
}()
|
||||
mainWindow.Run(f.startURL)
|
||||
mainWindow.Run(f.startURL.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -299,21 +287,48 @@ func (f *Frontend) ExecJS(js string) {
|
||||
func (f *Frontend) processRequest(r *request) {
|
||||
uri := C.GoString(r.url)
|
||||
|
||||
res, err := common.ProcessRequest(uri, f.assets, "wails", "wails")
|
||||
if err != nil {
|
||||
f.logger.Error("Error processing request '%s': %s (HttpResponse=%s)", uri, err, res)
|
||||
rw := httptest.NewRecorder()
|
||||
f.assets.ProcessHTTPRequest(
|
||||
uri,
|
||||
rw,
|
||||
func() (*http.Request, error) {
|
||||
req, err := r.GetHttpRequest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.URL.Host != f.startURL.Host {
|
||||
if req.Body != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Expected host '%d' in request, but was '%s'", f.startURL.Host, req.URL.Host)
|
||||
}
|
||||
return req, nil
|
||||
},
|
||||
)
|
||||
|
||||
header := map[string]string{}
|
||||
for k := range rw.Header() {
|
||||
header[k] = rw.Header().Get(k)
|
||||
}
|
||||
headerData, _ := json.Marshal(header)
|
||||
|
||||
var content unsafe.Pointer
|
||||
var contentLen int
|
||||
if _contents := res.Body; _contents != nil {
|
||||
if _contents := rw.Body.Bytes(); _contents != nil {
|
||||
content = unsafe.Pointer(&_contents[0])
|
||||
contentLen = len(_contents)
|
||||
}
|
||||
mimetype := C.CString(res.MimeType)
|
||||
defer C.free(unsafe.Pointer(mimetype))
|
||||
|
||||
C.ProcessURLResponse(r.ctx, r.url, C.int(res.StatusCode), mimetype, content, C.int(contentLen))
|
||||
var headers unsafe.Pointer
|
||||
var headersLen int
|
||||
if len(headerData) != 0 {
|
||||
headers = unsafe.Pointer(&headerData[0])
|
||||
headersLen = len(headerData)
|
||||
}
|
||||
|
||||
C.ProcessURLResponse(r.ctx, r.url, C.int(rw.Code), headers, C.int(headersLen), content, C.int(contentLen))
|
||||
}
|
||||
|
||||
//func (f *Frontend) processSystemEvent(message string) {
|
||||
@ -332,6 +347,40 @@ func (f *Frontend) processRequest(r *request) {
|
||||
// }
|
||||
//}
|
||||
|
||||
type request struct {
|
||||
url *C.char
|
||||
method string
|
||||
headers string
|
||||
body []byte
|
||||
|
||||
ctx unsafe.Pointer
|
||||
}
|
||||
|
||||
func (r *request) GetHttpRequest() (*http.Request, error) {
|
||||
var body io.Reader
|
||||
if len(r.body) != 0 {
|
||||
body = bytes.NewReader(r.body)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(r.method, C.GoString(r.url), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.headers != "" {
|
||||
var h map[string]string
|
||||
if err := json.Unmarshal([]byte(r.headers), &h); err != nil {
|
||||
return nil, fmt.Errorf("Unable to unmarshal request headers: %s", err)
|
||||
}
|
||||
|
||||
for k, v := range h {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
//export processMessage
|
||||
func processMessage(message *C.char) {
|
||||
goMessage := C.GoString(message)
|
||||
@ -339,10 +388,18 @@ func processMessage(message *C.char) {
|
||||
}
|
||||
|
||||
//export processURLRequest
|
||||
func processURLRequest(ctx unsafe.Pointer, url *C.char) {
|
||||
func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) {
|
||||
var goBody []byte
|
||||
if bodyLen != 0 {
|
||||
goBody = C.GoBytes(body, bodyLen)
|
||||
}
|
||||
|
||||
requestBuffer <- &request{
|
||||
url: url,
|
||||
ctx: ctx,
|
||||
url: url,
|
||||
method: C.GoString(method),
|
||||
headers: C.GoString(headers),
|
||||
body: goBody,
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,11 +29,11 @@ void processCallback(int callbackID) {
|
||||
NSLog(@"Process callback %d", callbackID);
|
||||
}
|
||||
|
||||
void processURLRequest(void *ctx, const char* url) {
|
||||
void processURLRequest(void *ctx, const char* url const char *method, const char *headers, const void *body, int bodyLen) {
|
||||
NSLog(@"processURLRequest called");
|
||||
const char myByteArray[] = { 0x3c,0x68,0x31,0x3e,0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0x3c,0x2f,0x68,0x31,0x3e };
|
||||
// void *inctx, const char *url, int statusCode, const char *contentType, void* data, int datalength
|
||||
ProcessURLResponse(ctx, url, 200, "text/html", (void*)myByteArray, 21);
|
||||
// void *inctx, const char *url, int statusCode, const char *headers, void* data, int datalength
|
||||
ProcessURLResponse(ctx, url, 200, "{\"Content-Type\": \"text/html\"}", (void*)myByteArray, 21);
|
||||
}
|
||||
|
||||
unsigned char _Users_username_Pictures_SaltBae_png[] = {
|
||||
|
@ -15,7 +15,7 @@ extern "C"
|
||||
#endif
|
||||
|
||||
void processMessage(const char *);
|
||||
void processURLRequest(void*, const char *);
|
||||
void processURLRequest(void*, const char *, const char *, const char *, const void *, int);
|
||||
void processMessageDialogResponse(int);
|
||||
void processOpenFileDialogResponse(const char*);
|
||||
void processSaveFileDialogResponse(const char*);
|
||||
|
@ -14,8 +14,10 @@ import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@ -25,7 +27,6 @@ import (
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/common"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
@ -41,13 +42,12 @@ type Frontend struct {
|
||||
|
||||
// Assets
|
||||
assets *assetserver.AssetServer
|
||||
startURL string
|
||||
startURL *url.URL
|
||||
|
||||
// main window handle
|
||||
mainWindow *Window
|
||||
bindings *binding.Bindings
|
||||
dispatcher frontend.Dispatcher
|
||||
servingFromDisk bool
|
||||
mainWindow *Window
|
||||
bindings *binding.Bindings
|
||||
dispatcher frontend.Dispatcher
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -65,23 +65,12 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
||||
bindings: appBindings,
|
||||
dispatcher: dispatcher,
|
||||
ctx: ctx,
|
||||
startURL: "wails://wails/",
|
||||
}
|
||||
result.startURL, _ = url.Parse("wails://wails/")
|
||||
|
||||
_starturl, _ := ctx.Value("starturl").(string)
|
||||
if _starturl != "" {
|
||||
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
|
||||
result.startURL = _starturl
|
||||
} else {
|
||||
// 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 by webkit.
|
||||
|
||||
_assetdir := ctx.Value("assetdir")
|
||||
if _assetdir != nil {
|
||||
result.servingFromDisk = true
|
||||
}
|
||||
|
||||
bindingsJSON, err := appBindings.ToJSON()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@ -93,7 +82,10 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
||||
}
|
||||
result.assets = assets
|
||||
|
||||
go result.startRequestProcessor()
|
||||
// Start 10 processors to handle requests in parallel
|
||||
for i := 0; i < 10; i++ {
|
||||
go result.startRequestProcessor()
|
||||
}
|
||||
}
|
||||
|
||||
go result.startMessageProcessor()
|
||||
@ -141,7 +133,7 @@ func (f *Frontend) Run(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
|
||||
f.mainWindow.Run(f.startURL)
|
||||
f.mainWindow.Run(f.startURL.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -300,11 +292,15 @@ var requestBuffer = make(chan unsafe.Pointer, 100)
|
||||
func (f *Frontend) startRequestProcessor() {
|
||||
for request := range requestBuffer {
|
||||
f.processRequest(request)
|
||||
C.g_object_unref(C.gpointer(request))
|
||||
}
|
||||
}
|
||||
|
||||
//export processURLRequest
|
||||
func processURLRequest(request unsafe.Pointer) {
|
||||
// Increment reference counter to allow async processing, will be decremented after the processing
|
||||
// has been finished by a worker.
|
||||
C.g_object_ref(C.gpointer(request))
|
||||
requestBuffer <- request
|
||||
}
|
||||
|
||||
@ -313,38 +309,29 @@ func (f *Frontend) processRequest(request unsafe.Pointer) {
|
||||
uri := C.webkit_uri_scheme_request_get_uri(req)
|
||||
goURI := C.GoString(uri)
|
||||
|
||||
res, err := common.ProcessRequest(goURI, f.assets, "wails", "wails")
|
||||
if err != nil {
|
||||
f.logger.Error("Error processing request '%s': %s (HttpResponse=%s)", goURI, err, res)
|
||||
}
|
||||
// WebKitGTK stable < 2.36 API does not support request method, request headers and request.
|
||||
// Apart from request bodies, this is only available beginning with 2.36: https://webkitgtk.org/reference/webkit2gtk/stable/WebKitURISchemeResponse.html
|
||||
rw := &webKitResponseWriter{req: req}
|
||||
defer rw.Close()
|
||||
|
||||
if code := res.StatusCode; code != http.StatusOK {
|
||||
message := C.CString(res.StatusText())
|
||||
gerr := C.g_error_new_literal(C.g_quark_from_string(message), C.int(code), message)
|
||||
C.webkit_uri_scheme_request_finish_error(req, gerr)
|
||||
C.g_error_free(gerr)
|
||||
C.free(unsafe.Pointer(message))
|
||||
return
|
||||
}
|
||||
f.assets.ProcessHTTPRequest(
|
||||
goURI,
|
||||
rw,
|
||||
func() (*http.Request, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, goURI, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cContent unsafe.Pointer
|
||||
bodyLen := len(res.Body)
|
||||
var cLen C.long
|
||||
if bodyLen > 0 {
|
||||
cContent = C.malloc(C.ulong(bodyLen))
|
||||
if cContent != nil {
|
||||
C.memcpy(cContent, unsafe.Pointer(&res.Body[0]), C.size_t(bodyLen))
|
||||
cLen = C.long(bodyLen)
|
||||
}
|
||||
}
|
||||
if req.URL.Host != f.startURL.Host {
|
||||
if req.Body != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
|
||||
cMimeType := C.CString(res.MimeType)
|
||||
defer C.free(unsafe.Pointer(cMimeType))
|
||||
return nil, fmt.Errorf("Expected host '%d' in request, but was '%s'", f.startURL.Host, req.URL.Host)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
})
|
||||
|
||||
stream := C.g_memory_input_stream_new_from_data(
|
||||
cContent,
|
||||
cLen,
|
||||
(*[0]byte)(C.free))
|
||||
C.webkit_uri_scheme_request_finish(req, stream, cLen, cMimeType)
|
||||
C.g_object_unref(C.gpointer(stream))
|
||||
}
|
||||
|
119
v2/internal/frontend/desktop/linux/responsewriter.go
Normal file
119
v2/internal/frontend/desktop/linux/responsewriter.go
Normal file
@ -0,0 +1,119 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package linux
|
||||
|
||||
/*
|
||||
#cgo linux pkg-config: gtk+-3.0 webkit2gtk-4.0 gio-unix-2.0
|
||||
|
||||
#include "gtk/gtk.h"
|
||||
#include "webkit2/webkit2.h"
|
||||
#include "gio/gunixinputstream.h"
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
|
||||
)
|
||||
|
||||
type webKitResponseWriter struct {
|
||||
req *C.WebKitURISchemeRequest
|
||||
|
||||
header http.Header
|
||||
wroteHeader bool
|
||||
|
||||
w io.WriteCloser
|
||||
wErr error
|
||||
}
|
||||
|
||||
func (rw *webKitResponseWriter) Header() http.Header {
|
||||
if rw.header == nil {
|
||||
rw.header = http.Header{}
|
||||
}
|
||||
return rw.header
|
||||
}
|
||||
|
||||
func (rw *webKitResponseWriter) Write(buf []byte) (int, error) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
if rw.wErr != nil {
|
||||
return 0, rw.wErr
|
||||
}
|
||||
return rw.w.Write(buf)
|
||||
}
|
||||
|
||||
func (rw *webKitResponseWriter) WriteHeader(code int) {
|
||||
if rw.wroteHeader {
|
||||
return
|
||||
}
|
||||
rw.wroteHeader = true
|
||||
|
||||
if code != http.StatusOK {
|
||||
// WebKitGTK stable < 2.36 API does not support response headers and response statuscodes
|
||||
rw.w = &nopCloser{io.Discard}
|
||||
rw.finishWithError(http.StatusText(code), code)
|
||||
return
|
||||
}
|
||||
|
||||
// We can't use os.Pipe here, because that returns files with a finalizer for closing the FD. But the control over the
|
||||
// read FD is given to the InputStream and will be closed there.
|
||||
// Furthermore we especially don't want to have the FD_CLOEXEC
|
||||
rFD, w, err := pipe()
|
||||
if err != nil {
|
||||
rw.wErr = fmt.Errorf("Unable opening pipe: %s", err)
|
||||
rw.finishWithError(rw.wErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rw.w = w
|
||||
|
||||
cMimeType := C.CString(rw.Header().Get(assetserver.HeaderContentType))
|
||||
defer C.free(unsafe.Pointer(cMimeType))
|
||||
|
||||
contentLength := int64(-1)
|
||||
if sLen := rw.Header().Get(assetserver.HeaderContentLength); sLen != "" {
|
||||
if pLen, _ := strconv.ParseInt(sLen, 10, 64); pLen > 0 {
|
||||
contentLength = pLen
|
||||
}
|
||||
}
|
||||
|
||||
stream := C.g_unix_input_stream_new(C.int(rFD), gtkBool(true))
|
||||
C.webkit_uri_scheme_request_finish(rw.req, stream, C.long(contentLength), cMimeType)
|
||||
C.g_object_unref(C.gpointer(stream))
|
||||
}
|
||||
|
||||
func (rw *webKitResponseWriter) Close() {
|
||||
if rw.w != nil {
|
||||
rw.w.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *webKitResponseWriter) finishWithError(message string, code int) {
|
||||
msg := C.CString(http.StatusText(code))
|
||||
gerr := C.g_error_new_literal(C.g_quark_from_string(msg), C.int(code), msg)
|
||||
C.webkit_uri_scheme_request_finish_error(rw.req, gerr)
|
||||
C.g_error_free(gerr)
|
||||
C.free(unsafe.Pointer(msg))
|
||||
}
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error { return nil }
|
||||
|
||||
func pipe() (r int, w *os.File, err error) {
|
||||
var p [2]int
|
||||
e := syscall.Pipe2(p[0:], 0)
|
||||
if e != nil {
|
||||
return 0, nil, fmt.Errorf("pipe2: %s", e)
|
||||
}
|
||||
|
||||
return p[0], os.NewFile(uintptr(p[1]), "|1"), nil
|
||||
}
|
@ -7,7 +7,11 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -16,7 +20,6 @@ import (
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/common"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/go-webview2/pkg/edge"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
|
||||
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
|
||||
@ -38,13 +41,12 @@ type Frontend struct {
|
||||
|
||||
// Assets
|
||||
assets *assetserver.AssetServer
|
||||
startURL string
|
||||
startURL *url.URL
|
||||
|
||||
// main window handle
|
||||
mainWindow *Window
|
||||
bindings *binding.Bindings
|
||||
dispatcher frontend.Dispatcher
|
||||
servingFromDisk bool
|
||||
mainWindow *Window
|
||||
bindings *binding.Bindings
|
||||
dispatcher frontend.Dispatcher
|
||||
|
||||
hasStarted bool
|
||||
|
||||
@ -63,26 +65,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
||||
bindings: appBindings,
|
||||
dispatcher: dispatcher,
|
||||
ctx: ctx,
|
||||
startURL: "http://wails.localhost/",
|
||||
versionInfo: versionInfo,
|
||||
}
|
||||
|
||||
_starturl, _ := ctx.Value("starturl").(string)
|
||||
if _starturl != "" {
|
||||
// We currently can't use wails://wails/ as other platforms do, therefore we map the assets sever onto the following url.
|
||||
result.startURL, _ = url.Parse("http://wails.localhost/")
|
||||
|
||||
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
|
||||
result.startURL = _starturl
|
||||
return result
|
||||
}
|
||||
|
||||
// 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 by WebView2 in dev mode
|
||||
|
||||
_assetdir := ctx.Value("assetdir")
|
||||
if _assetdir != nil {
|
||||
result.servingFromDisk = true
|
||||
}
|
||||
|
||||
bindingsJSON, err := appBindings.ToJSON()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@ -372,7 +365,7 @@ func (f *Frontend) setupChromium() {
|
||||
|
||||
chromium.SetGlobalPermission(edge.CoreWebView2PermissionStateAllow)
|
||||
chromium.AddWebResourceRequestedFilter("*", edge.COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL)
|
||||
chromium.Navigate(f.startURL)
|
||||
chromium.Navigate(f.startURL.String())
|
||||
}
|
||||
|
||||
type EventNotify struct {
|
||||
@ -403,34 +396,39 @@ func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, arg
|
||||
reqHeaders.Release()
|
||||
}
|
||||
|
||||
if f.assets == nil {
|
||||
// We are using the devServer let the WebView2 handle the request with its default handler
|
||||
return
|
||||
}
|
||||
|
||||
//Get the request
|
||||
uri, _ := req.GetUri()
|
||||
|
||||
res, err := common.ProcessRequest(uri, f.assets, "http", "wails.localhost")
|
||||
if err == common.ErrUnexpectedScheme {
|
||||
// In this case we should let the WebView2 handle the request with its default handler
|
||||
reqUri, err := url.ParseRequestURI(uri)
|
||||
if err != nil {
|
||||
f.logger.Error("Unable to parse equest uri %s: %s", uri, err)
|
||||
return
|
||||
} else if err == common.ErrUnexpectedHost {
|
||||
// This means file:// to something other than wails, should we prevent this?
|
||||
// Maybe we should introduce an AllowList for explicitly allowing schemes and hosts, this could also be interesting
|
||||
// for all other platforms to improve security.
|
||||
return // Let WebView2 handle the request with its default handler
|
||||
} else if err != nil {
|
||||
path := strings.Replace(uri, "http://wails.localhost", "", 1)
|
||||
f.logger.Error("Error processing request '%s': %s (HttpResponse=%s)", path, err, res)
|
||||
}
|
||||
|
||||
if reqUri.Scheme != f.startURL.Scheme {
|
||||
// Let the WebView2 handle the request with its default handler
|
||||
return
|
||||
} else if reqUri.Host != f.startURL.Host {
|
||||
// Let the WebView2 handle the request with its default handler
|
||||
return
|
||||
}
|
||||
|
||||
logInfo := strings.Replace(uri, f.startURL.String(), "", 1)
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
f.assets.ProcessHTTPRequest(logInfo, rw, coreWebview2RequestToHttpRequest(req))
|
||||
|
||||
headers := []string{}
|
||||
if mimeType := res.MimeType; mimeType != "" {
|
||||
headers = append(headers, "Content-Type: "+mimeType)
|
||||
}
|
||||
content := res.Body
|
||||
if content != nil && f.servingFromDisk {
|
||||
headers = append(headers, "Pragma: no-cache")
|
||||
for k, v := range rw.Header() {
|
||||
headers = append(headers, fmt.Sprintf("%s: %s", k, strings.Join(v, ",")))
|
||||
}
|
||||
|
||||
env := f.chromium.Environment()
|
||||
response, err := env.CreateWebResourceResponse(content, res.StatusCode, res.StatusText(), strings.Join(headers, "\n"))
|
||||
response, err := env.CreateWebResourceResponse(rw.Body.Bytes(), rw.Code, http.StatusText(rw.Code), strings.Join(headers, "\n"))
|
||||
if err != nil {
|
||||
f.logger.Error("CreateWebResourceResponse Error: %s", err)
|
||||
return
|
||||
@ -596,3 +594,87 @@ func (f *Frontend) ShowWindow() {
|
||||
func (f *Frontend) onFocus(arg *winc.Event) {
|
||||
f.chromium.Focus()
|
||||
}
|
||||
|
||||
func coreWebview2RequestToHttpRequest(coreReq *edge.ICoreWebView2WebResourceRequest) func() (*http.Request, error) {
|
||||
return func() (r *http.Request, err error) {
|
||||
header := http.Header{}
|
||||
headers, err := coreReq.GetHeaders()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetHeaders Error: %s", err)
|
||||
}
|
||||
defer headers.Release()
|
||||
|
||||
headersIt, err := headers.GetIterator()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetIterator Error: %s", err)
|
||||
}
|
||||
defer headersIt.Release()
|
||||
|
||||
for {
|
||||
has, err := headersIt.HasCurrentHeader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HasCurrentHeader Error: %s", err)
|
||||
}
|
||||
if !has {
|
||||
break
|
||||
}
|
||||
|
||||
name, value, err := headersIt.GetCurrentHeader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetCurrentHeader Error: %s", err)
|
||||
}
|
||||
|
||||
header.Set(name, value)
|
||||
if _, err := headersIt.MoveNext(); err != nil {
|
||||
return nil, fmt.Errorf("MoveNext Error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
method, err := coreReq.GetMethod()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetMethod Error: %s", err)
|
||||
}
|
||||
|
||||
uri, err := coreReq.GetUri()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUri Error: %s", err)
|
||||
}
|
||||
|
||||
var body io.ReadCloser
|
||||
if content, err := coreReq.GetContent(); err != nil {
|
||||
return nil, fmt.Errorf("GetContent Error: %s", err)
|
||||
} else if content != nil {
|
||||
body = &iStreamReleaseCloser{stream: content}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, uri, body)
|
||||
if err != nil {
|
||||
if body != nil {
|
||||
body.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
req.Header = header
|
||||
return req, nil
|
||||
}
|
||||
}
|
||||
|
||||
type iStreamReleaseCloser struct {
|
||||
stream *edge.IStream
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (i *iStreamReleaseCloser) Read(p []byte) (int, error) {
|
||||
if i.closed {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return i.stream.Read(p)
|
||||
}
|
||||
|
||||
func (i *iStreamReleaseCloser) Close() error {
|
||||
if i.closed {
|
||||
return nil
|
||||
}
|
||||
i.closed = true
|
||||
return i.stream.Release()
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -84,12 +83,16 @@ func (d *DevWebServer) Run(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if externalURL.Host == "" {
|
||||
return fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?")
|
||||
}
|
||||
|
||||
waitCb := func() { d.LogDebug("Waiting for frontend DevServer '%s' to be ready", externalURL) }
|
||||
if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) {
|
||||
d.logger.Error("Timeout waiting for frontend DevServer")
|
||||
}
|
||||
|
||||
assetHandler = httputil.NewSingleHostReverseProxy(externalURL)
|
||||
assetHandler = newExternalDevServerAssetHandler(d.logger, externalURL, d.appoptions.AssetsHandler)
|
||||
}
|
||||
|
||||
// Setup internal dev server
|
||||
@ -103,7 +106,7 @@ func (d *DevWebServer) Run(ctx context.Context) error {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
d.server.GET("/*", func(c echo.Context) error {
|
||||
d.server.Any("/*", func(c echo.Context) error {
|
||||
assetServer.ServeHTTP(c.Response(), c.Request())
|
||||
return nil
|
||||
})
|
||||
|
78
v2/internal/frontend/devserver/external.go
Normal file
78
v2/internal/frontend/devserver/external.go
Normal file
@ -0,0 +1,78 @@
|
||||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package devserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
)
|
||||
|
||||
func newExternalDevServerAssetHandler(logger *logger.Logger, url *url.URL, handler http.Handler) http.Handler {
|
||||
errSkipProxy := fmt.Errorf("skip proxying")
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(url)
|
||||
baseDirector := proxy.Director
|
||||
proxy.Director = func(r *http.Request) {
|
||||
baseDirector(r)
|
||||
if logger != nil {
|
||||
logger.Debug("[ExternalAssetHandler] Loading '%s'", r.URL)
|
||||
}
|
||||
}
|
||||
|
||||
proxy.ModifyResponse = func(res *http.Response) error {
|
||||
if handler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusSwitchingProtocols {
|
||||
return nil
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed {
|
||||
return errSkipProxy
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
|
||||
if handler != nil && errors.Is(err, errSkipProxy) {
|
||||
if logger != nil {
|
||||
logger.Debug("[ExternalAssetHandler] Loading '%s' failed, using AssetHandler", r.URL)
|
||||
}
|
||||
handler.ServeHTTP(rw, r)
|
||||
} else {
|
||||
if logger != nil {
|
||||
logger.Error("[ExternalAssetHandler] Proxy error: %v", err)
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
e := echo.New()
|
||||
e.Any("/*",
|
||||
func(c echo.Context) error {
|
||||
req := c.Request()
|
||||
rw := c.Response()
|
||||
if c.IsWebSocket() || req.Method == http.MethodGet {
|
||||
proxy.ServeHTTP(rw, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
if handler != nil {
|
||||
handler.ServeHTTP(rw, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusMethodNotAllowed)
|
||||
})
|
||||
|
||||
return e
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/options/linux"
|
||||
@ -42,6 +43,7 @@ type App struct {
|
||||
AlwaysOnTop bool
|
||||
RGBA *RGBA
|
||||
Assets fs.FS
|
||||
AssetsHandler http.Handler
|
||||
Menu *menu.Menu
|
||||
Logger logger.Logger `json:"-"`
|
||||
LogLevel logger.LogLevel
|
||||
|
@ -161,6 +161,15 @@ The second, if given, will be executed in the `frontend` directory to build the
|
||||
|
||||
If these 2 keys aren't given, then Wails does absolutely nothing with the frontend. It is only expecting that `embed.FS`.
|
||||
|
||||
### AssetsHandler
|
||||
|
||||
A Wails v2 app can optionally define a `http.Handler` in the `options.App`, which allows hooking into the AssetServer to
|
||||
create files on the fly or process POST/PUT requests.
|
||||
GET requests are always first handled by the `assets` FS. If the FS doesn't find the requested file the request will be
|
||||
forwarded to the `http.Handler` for serving. Any requests other than GET will be directly processed by the `AssetsHandler`
|
||||
if specified.
|
||||
It's also possible to only use the `AssetsHandler` by specifiy `nil` as the `Assets` option.
|
||||
|
||||
## Built in Dev Server
|
||||
|
||||
Running `wails dev` will start the built in dev server which will start a file watcher in your project directory. By
|
||||
|
@ -30,6 +30,7 @@ func main() {
|
||||
RGBA: &options.RGBA{R: 0, G: 0, B: 0, A: 255},
|
||||
AlwaysOnTop: false,
|
||||
Assets: assets,
|
||||
AssetsHandler: assetsHandler,
|
||||
Menu: app.applicationMenu(),
|
||||
Logger: nil,
|
||||
LogLevel: logger.DEBUG,
|
||||
@ -221,6 +222,36 @@ Type: \*embed.FS
|
||||
|
||||
The frontend assets to be used by the application. Requires an `index.html` file.
|
||||
|
||||
### AssetsHandler
|
||||
|
||||
Name: AssetsHandler
|
||||
|
||||
Type: \*http.Handler
|
||||
|
||||
The assets handler is a generic `http.Handler` which will be called for any non GET request on the assets server
|
||||
and for GET requests which can not be served from the `assets` because the file is not found.
|
||||
|
||||
| Value | Win | Mac | Lin |
|
||||
| ----------------------------- | --- | --- | --- |
|
||||
| GET | ✅ | ✅ | ✅ |
|
||||
| POST | ✅ | ✅ | ❌ |
|
||||
| PUT | ✅ | ✅ | ❌ |
|
||||
| PATCH | ✅ | ✅ | ❌ |
|
||||
| DELETE | ✅ | ✅ | ❌ |
|
||||
| Request Headers | ✅ | ✅ | ❌ |
|
||||
| Request Body | ✅ | ✅ | ❌ |
|
||||
| Request Body Streaming | ❌ | ❌ | ❌ |
|
||||
| Response StatusCodes | ✅ | ✅ | ❌ |
|
||||
| Response Headers | ✅ | ✅ | ❌ |
|
||||
| Response Body | ✅ | ✅ | ✅ |
|
||||
| Response Body Streaming | ❌ | ❌ | ✅ |
|
||||
|
||||
NOTE: Linux is currently very limited due to targeting a WebKit2GTK Version < 2.36.0. In the future some features will be
|
||||
supported by the introduction of WebKit2GTK 2.36.0+ support.
|
||||
|
||||
NOTE: When used in combination with a Frontend DevServer there might be limitations, eg. Vite serves the index.html
|
||||
on every path, that does not contain a file extension.
|
||||
|
||||
### Menu
|
||||
|
||||
Name: Menu
|
||||
@ -316,7 +347,7 @@ Defines how the window should present itself at startup.
|
||||
| --------------- | --- | --- | --- |
|
||||
| Fullscreen | ✅ | ✅ | ✅ |
|
||||
| Maximised | ✅ | ✅ | ✅ |
|
||||
| Minimised | ✅ | | ✅ |
|
||||
| Minimised | ✅ | ❌ | ✅ |
|
||||
|
||||
### Bind
|
||||
|
||||
|
@ -20,7 +20,7 @@ The project config resides in the `wails.json` file in the project directory. Th
|
||||
"version": "[Project config version]",
|
||||
"outputfilename": "[The name of the binary]",
|
||||
"debounceMS": 100, // The default time the dev server waits to reload when it detects a vhange in assets
|
||||
"devServer": "[Address to bind the wails dev sever to. Default: http://localhost:34115]",
|
||||
"devServer": "[Address to bind the wails dev sever to. Default: localhost:34115]",
|
||||
"appargs": "[Arguments passed to the application in shell style when in dev mode]",
|
||||
"runNonNativeBuildHooks": false, // Defines if build hooks should be run though they are defined for an OS other than the host OS.
|
||||
"postBuildHooks": {
|
||||
|
@ -20,7 +20,7 @@ sidebar_position: 5
|
||||
"version": "[项目配置版本]",
|
||||
"outputfilename": "[二进制文件的名称]",
|
||||
"debounceMS": 100, // 在检测到资源更改时,开发服务器等待重新加载的时间
|
||||
"devServer": "[将 wails 开发服务器绑定到的地址。默认:http://localhost:34115]",
|
||||
"devServer": "[将 wails 开发服务器绑定到的地址。默认:localhost:34115]",
|
||||
"appargs": "[在dev模式下以shell样式传递给应用程序的参数]",
|
||||
"runNonNativeBuildHooks": false, // 定义构建钩子是否应该运行,尽管它们是为主机操作系统以外的操作系统定义的。
|
||||
"postBuildHooks": {
|
||||
|
Loading…
Reference in New Issue
Block a user