diff --git a/v2/internal/frontend/desktop/darwin/Application.h b/v2/internal/frontend/desktop/darwin/Application.h index 2432e8906..e418168e6 100644 --- a/v2/internal/frontend/desktop/darwin/Application.h +++ b/v2/internal/frontend/desktop/darwin/Application.h @@ -48,11 +48,6 @@ const bool IsFullScreen(void *ctx); const bool IsMinimised(void *ctx); const bool IsMaximised(void *ctx); -void ProcessURLDidReceiveResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength); -bool ProcessURLDidReceiveData(void *inctx, unsigned long long requestId, void* data, int datalength); -void ProcessURLDidFinish(void *inctx, unsigned long long requestId); -int ProcessURLRequestReadBodyStream(void *inctx, unsigned long long requestId, void *buf, int bufLen); - /* Dialogs */ void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength); diff --git a/v2/internal/frontend/desktop/darwin/Application.m b/v2/internal/frontend/desktop/darwin/Application.m index 9b4fe502b..ab951714d 100644 --- a/v2/internal/frontend/desktop/darwin/Application.m +++ b/v2/internal/frontend/desktop/darwin/Application.m @@ -52,36 +52,6 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in return result; } -void ProcessURLDidReceiveResponse(void *inctx, unsigned long long requestId, int statusCode, void *headersString, int headersStringLength) { - WailsContext *ctx = (__bridge WailsContext*) inctx; - @autoreleasepool { - NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength]; - [ctx processURLDidReceiveResponse:requestId :statusCode :nsHeadersJSON]; - } -} - -bool ProcessURLDidReceiveData(void *inctx, unsigned long long requestId, void* data, int datalength) { - WailsContext *ctx = (__bridge WailsContext*) inctx; - @autoreleasepool { - NSData *nsdata = [NSData dataWithBytes:data length:datalength]; - return [ctx processURLDidReceiveData:requestId :nsdata]; - } -} - -void ProcessURLDidFinish(void *inctx, unsigned long long requestId) { - WailsContext *ctx = (__bridge WailsContext*) inctx; - @autoreleasepool { - [ctx processURLDidFinish:requestId]; - } -} - -int ProcessURLRequestReadBodyStream(void *inctx, unsigned long long requestId, void *buf, int bufLen) { - WailsContext *ctx = (__bridge WailsContext*) inctx; - @autoreleasepool { - return [ctx processURLRequestReadBodyStream:requestId :buf :bufLen]; - } -} - void ExecJS(void* inctx, const char *script) { WailsContext *ctx = (__bridge WailsContext*) inctx; NSString *nsscript = safeInit(script); diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.h b/v2/internal/frontend/desktop/darwin/WailsContext.h index cd187744d..1e48b2182 100644 --- a/v2/internal/frontend/desktop/darwin/WailsContext.h +++ b/v2/internal/frontend/desktop/darwin/WailsContext.h @@ -47,9 +47,6 @@ @property bool debug; @property (retain) WKUserContentController* userContentController; -@property (retain) NSLock *urlRequestsLock; -@property unsigned long long urlRequestsId; -@property (retain) NSMutableDictionary *urlRequests; @property (retain) NSMenu* applicationMenu; @@ -89,10 +86,6 @@ - (void) SaveFileDialog :(NSString*)title :(NSString*)defaultFilename :(NSString*)defaultDirectory :(bool)canCreateDirectories :(bool)treatPackagesAsDirectories :(bool)showHiddenFiles :(NSString*)filters; - (void) loadRequest:(NSString*)url; -- (void) processURLDidReceiveResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON; -- (bool) processURLDidReceiveData:(unsigned long long)requestId :(NSData *)data; -- (void) processURLDidFinish:(unsigned long long)requestId; -- (int) processURLRequestReadBodyStream:(unsigned long long)requestId :(void *)buf :(int)bufLen; - (void) ExecJS:(NSString*)script; - (NSScreen*) getCurrentScreen; diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.m b/v2/internal/frontend/desktop/darwin/WailsContext.m index d01331623..29fa99317 100644 --- a/v2/internal/frontend/desktop/darwin/WailsContext.m +++ b/v2/internal/frontend/desktop/darwin/WailsContext.m @@ -108,7 +108,6 @@ typedef void (^schemeTaskCaller)(id); [self.mainWindow release]; [self.mouseEvent release]; [self.userContentController release]; - [self.urlRequests release]; [self.applicationMenu release]; [super dealloc]; } @@ -138,9 +137,6 @@ typedef void (^schemeTaskCaller)(id); } - (void) CreateWindow:(int)width :(int)height :(bool)frameless :(bool)resizable :(bool)fullscreen :(bool)fullSizeContent :(bool)hideTitleBar :(bool)titlebarAppearsTransparent :(bool)hideTitle :(bool)useToolbar :(bool)hideToolbarSeparator :(bool)webviewIsTransparent :(bool)hideWindowOnClose :(NSString*)appearance :(bool)windowIsTranslucent :(int)minWidth :(int)minHeight :(int)maxWidth :(int)maxHeight :(bool)fraudulentWebsiteWarningEnabled { - self.urlRequestsId = 0; - self.urlRequests = [NSMutableDictionary new]; - NSWindowStyleMask styleMask = 0; if( !frameless ) { @@ -431,156 +427,19 @@ typedef void (^schemeTaskCaller)(id); }]; } -- (void) processURLDidReceiveResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON { - [self processURLSchemeTaskCall:requestId :^(id urlSchemeTask) { - NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData: headersJSON options: NSJSONReadingMutableContainers error: nil]; - NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease]; - - [urlSchemeTask didReceiveResponse:response]; - }]; -} - -- (bool) processURLDidReceiveData:(unsigned long long)requestId :(NSData *)data { - return [self processURLSchemeTaskCall:requestId :^(id urlSchemeTask) { - [urlSchemeTask didReceiveData:data]; - }]; -} - -- (void) processURLDidFinish:(unsigned long long)requestId { - [self processURLSchemeTaskCall:requestId :^(id urlSchemeTask) { - [urlSchemeTask didFinish]; - }]; - - NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId]; - [self removeURLSchemeTask:key]; -} - -- (int) processURLRequestReadBodyStream:(unsigned long long)requestId :(void *)buf :(int)bufLen { - int res = 0; - int *pRes = &res; - - bool hasRequest = [self processURLSchemeTaskCall:requestId :^(id urlSchemeTask) { - NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream; - if (!stream) { - *pRes = -3; - } else { - NSStreamStatus status = stream.streamStatus; - if (status == NSStreamStatusAtEnd) { - *pRes = 0; - } else if (status != NSStreamStatusOpen) { - *pRes = -4; - } else if (!stream.hasBytesAvailable) { - *pRes = 0; - } else { - *pRes = [stream read:buf maxLength:bufLen]; - } - } - }]; - - if (!hasRequest) { - res = -2; - } - - return res; -} - - (void)webView:(nonnull WKWebView *)webView startURLSchemeTask:(nonnull id)urlSchemeTask { // This callback is run with an autorelease pool - const char *url = [urlSchemeTask.request.URL.absoluteString UTF8String]; - const char *method = [urlSchemeTask.request.HTTPMethod UTF8String]; - const char *headerJSON = ""; - const void *body = nil; - int bodyLen = 0; - int hasBodyStream = 0; - - 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 if (urlSchemeTask.request.HTTPBodyStream) { - hasBodyStream = 1; - [urlSchemeTask.request.HTTPBodyStream open]; - } - - unsigned long long requestId; - @synchronized(self.urlRequests) { - self.urlRequestsId++; - requestId = self.urlRequestsId; - NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId]; - self.urlRequests[key] = urlSchemeTask; - } - - processURLRequest(self, requestId, url, method, headerJSON, body, bodyLen, hasBodyStream); + processURLRequest(self, urlSchemeTask); } - (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id)urlSchemeTask { - NSArray *keys; - @synchronized(self.urlRequests) { - keys = [self.urlRequests allKeys]; - } - - for (NSNumber *key in keys) { - if (self.urlRequests[key] == urlSchemeTask) { - [self removeURLSchemeTask:key]; - } - } -} - -- (void) removeURLSchemeTask:(NSNumber *)urlSchemeTaskKey { - id urlSchemeTask = nil; - @synchronized(self.urlRequests) { - urlSchemeTask = self.urlRequests[urlSchemeTaskKey]; - } - - if (!urlSchemeTask) { - return; - } - NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream; if (stream) { - [stream close]; - } - - @synchronized(self.urlRequests) { - [self.urlRequests removeObjectForKey:urlSchemeTaskKey]; - } -} - -- (bool)processURLSchemeTaskCall:(unsigned long long)requestId :(schemeTaskCaller)fn { - NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId]; - - id urlSchemeTask; - @synchronized(self.urlRequests) { - urlSchemeTask = self.urlRequests[key]; - } - - if (urlSchemeTask == nil) { - // Stopped task, drop content... - return false; - } - - @try { - fn(urlSchemeTask); - } @catch (NSException *exception) { - [self removeURLSchemeTask:key]; - - // This is very bad to detect a stopped schemeTask this should be implemented in a better way - // But it seems to be very tricky to not deadlock when keeping a lock curing executing fn() - // It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want - // to get the lock again to start another request or stop it. - if ([exception.reason isEqualToString: @"This task has already been stopped"]) { - return false; + NSStreamStatus status = stream.streamStatus; + if (status != NSStreamStatusClosed && status != NSStreamStatusNotOpen) { + [stream close]; } - - @throw exception; } - - return true; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index 07437a541..7295e0ee6 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -19,11 +19,12 @@ import ( "fmt" "html/template" "log" - "net/http" "net/url" "strconv" + "unsafe" "github.com/wailsapp/wails/v2/pkg/assetserver" + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/frontend" @@ -35,7 +36,7 @@ import ( const startURL = "wails://wails/" var messageBuffer = make(chan string, 100) -var requestBuffer = make(chan *wkWebViewRequest, 100) +var requestBuffer = make(chan webview.Request, 100) var callbackBuffer = make(chan uint, 10) type Frontend struct { @@ -93,12 +94,10 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. if err != nil { log.Fatal(err) } + assets.ExpectedWebViewHost = result.startURL.Host result.assets = assets - // Start 10 processors to handle requests in parallel - for i := 0; i < 10; i++ { - go result.startRequestProcessor() - } + go result.startRequestProcessor() } go result.startMessageProcessor() @@ -114,7 +113,8 @@ func (f *Frontend) startMessageProcessor() { } func (f *Frontend) startRequestProcessor() { for request := range requestBuffer { - f.processRequest(request) + f.assets.ServeWebViewRequest(request) + request.Release() } } func (f *Frontend) startCallbackProcessor() { @@ -344,31 +344,6 @@ func (f *Frontend) ExecJS(js string) { f.mainWindow.ExecJS(js) } -func (f *Frontend) processRequest(r *wkWebViewRequest) { - rw := &wkWebViewResponseWriter{r: r} - defer rw.Close() - - f.assets.ProcessHTTPRequest( - r.url, - 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 '%s' in request, but was '%s'", f.startURL.Host, req.URL.Host) - } - return req, nil - }, - ) -} - //func (f *Frontend) processSystemEvent(message string) { // sl := strings.Split(message, ":") // if len(sl) != 2 { @@ -395,3 +370,8 @@ func processMessage(message *C.char) { func processCallback(callbackID uint) { callbackBuffer <- callbackID } + +//export processURLRequest +func processURLRequest(ctx unsafe.Pointer, wkURLSchemeTask unsafe.Pointer) { + requestBuffer <- webview.NewRequest(wkURLSchemeTask) +} diff --git a/v2/internal/frontend/desktop/darwin/message.h b/v2/internal/frontend/desktop/darwin/message.h index 731fca37c..66110841d 100644 --- a/v2/internal/frontend/desktop/darwin/message.h +++ b/v2/internal/frontend/desktop/darwin/message.h @@ -15,7 +15,7 @@ extern "C" #endif void processMessage(const char *); -void processURLRequest(void*, unsigned long long, const char *, const char *, const char *, const void *, int, int); +void processURLRequest(void *, void*); void processMessageDialogResponse(int); void processOpenFileDialogResponse(const char*); void processSaveFileDialogResponse(const char*); diff --git a/v2/internal/frontend/desktop/darwin/wkwebview_request.go b/v2/internal/frontend/desktop/darwin/wkwebview_request.go deleted file mode 100644 index ce8211970..000000000 --- a/v2/internal/frontend/desktop/darwin/wkwebview_request.go +++ /dev/null @@ -1,106 +0,0 @@ -//go:build darwin - -package darwin - -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit - -#import "Application.h" -#include -*/ -import "C" -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "unsafe" -) - -//export processURLRequest -func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int, hasBodyStream C.int) { - var bodyReader io.Reader - if body != nil && bodyLen != 0 { - bodyReader = bytes.NewReader(C.GoBytes(body, bodyLen)) - } else if hasBodyStream != 0 { - bodyReader = &bodyStreamReader{id: requestId, ctx: ctx} - } - - requestBuffer <- &wkWebViewRequest{ - id: requestId, - url: C.GoString(url), - method: C.GoString(method), - headers: C.GoString(headers), - body: bodyReader, - ctx: ctx, - } -} - -type wkWebViewRequest struct { - id C.ulonglong - url string - method string - headers string - body io.Reader - - ctx unsafe.Pointer -} - -func (r *wkWebViewRequest) GetHttpRequest() (*http.Request, error) { - req, err := http.NewRequest(r.method, r.url, r.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 -} - -var _ io.Reader = &bodyStreamReader{} - -type bodyStreamReader struct { - id C.ulonglong - ctx unsafe.Pointer -} - -// Read implements io.Reader -func (r *bodyStreamReader) Read(p []byte) (n int, err error) { - var content unsafe.Pointer - var contentLen int - if p != nil { - content = unsafe.Pointer(&p[0]) - contentLen = len(p) - } - - res := C.ProcessURLRequestReadBodyStream(r.ctx, r.id, content, C.int(contentLen)) - if res > 0 { - return int(res), nil - } - - switch res { - case 0: - return 0, io.EOF - case -1: - return 0, fmt.Errorf("body: stream error") - case -2: - return 0, errRequestStopped - case -3: - return 0, fmt.Errorf("body: no stream defined") - case -4: - return 0, io.ErrClosedPipe - default: - return 0, fmt.Errorf("body: unknown error %d", res) - } -} diff --git a/v2/internal/frontend/desktop/darwin/wkwebview_responsewriter.go b/v2/internal/frontend/desktop/darwin/wkwebview_responsewriter.go deleted file mode 100644 index b4110ed42..000000000 --- a/v2/internal/frontend/desktop/darwin/wkwebview_responsewriter.go +++ /dev/null @@ -1,81 +0,0 @@ -//go:build darwin - -package darwin - -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit - -#import "Application.h" -#include -*/ -import "C" -import ( - "encoding/json" - "errors" - "net/http" - "unsafe" -) - -var ( - errRequestStopped = errors.New("request has been stopped") -) - -type wkWebViewResponseWriter struct { - r *wkWebViewRequest - - header http.Header - wroteHeader bool -} - -func (rw *wkWebViewResponseWriter) Header() http.Header { - if rw.header == nil { - rw.header = http.Header{} - } - return rw.header -} - -func (rw *wkWebViewResponseWriter) Write(buf []byte) (int, error) { - rw.WriteHeader(http.StatusOK) - - var content unsafe.Pointer - var contentLen int - if buf != nil { - content = unsafe.Pointer(&buf[0]) - contentLen = len(buf) - } - - if !C.ProcessURLDidReceiveData(rw.r.ctx, rw.r.id, content, C.int(contentLen)) { - return 0, errRequestStopped - } - return contentLen, nil -} - -func (rw *wkWebViewResponseWriter) WriteHeader(code int) { - if rw.wroteHeader { - return - } - rw.wroteHeader = true - - header := map[string]string{} - for k := range rw.Header() { - header[k] = rw.Header().Get(k) - } - headerData, _ := json.Marshal(header) - - var headers unsafe.Pointer - var headersLen int - if len(headerData) != 0 { - headers = unsafe.Pointer(&headerData[0]) - headersLen = len(headerData) - } - - C.ProcessURLDidReceiveResponse(rw.r.ctx, rw.r.id, C.int(code), headers, C.int(headersLen)) -} - -func (rw *wkWebViewResponseWriter) Close() { - if !rw.wroteHeader { - rw.WriteHeader(http.StatusNotImplemented) - } - C.ProcessURLDidFinish(rw.r.ctx, rw.r.id) -} diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index dbacc4e6d..07d270f13 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -486,8 +486,7 @@ func (f *Frontend) processRequest(request unsafe.Pointer) { rw := &webKitResponseWriter{req: req} defer rw.Close() - f.assets.ProcessHTTPRequest( - goURI, + f.assets.ProcessHTTPRequestLegacy( rw, func() (*http.Request, error) { method := webkit_uri_scheme_request_get_http_method(req) diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index 05b183260..bf1c7d7c5 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -547,10 +547,8 @@ func (f *Frontend) processRequest(req *edge.ICoreWebView2WebResourceRequest, arg return } - logInfo := strings.Replace(uri, f.startURL.String(), "", 1) - rw := httptest.NewRecorder() - f.assets.ProcessHTTPRequest(logInfo, rw, coreWebview2RequestToHttpRequest(req)) + f.assets.ProcessHTTPRequestLegacy(rw, coreWebview2RequestToHttpRequest(req)) headers := []string{} for k, v := range rw.Header() { diff --git a/v2/pkg/assetserver/assetserver.go b/v2/pkg/assetserver/assetserver.go index f02163b59..1d085df0a 100644 --- a/v2/pkg/assetserver/assetserver.go +++ b/v2/pkg/assetserver/assetserver.go @@ -2,10 +2,8 @@ package assetserver import ( "bytes" - "fmt" "net/http" "net/http/httptest" - "strconv" "golang.org/x/net/html" @@ -35,6 +33,8 @@ type AssetServer struct { servingFromDisk bool appendSpinnerToBody bool + + assetServerWebView } func NewAssetServerMainPage(bindingsJSON string, options *options.App, servingFromDisk bool, logger Logger, runtime RuntimeAssets) (*AssetServer, error) { @@ -134,47 +134,6 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } -// 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 := reqGetter() - if err != nil { - 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 - } - - d.ServeHTTP(rw, req) - 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) { htmlNode, err := getHTMLNode(indexHTML) if err != nil { diff --git a/v2/pkg/assetserver/assetserver_legacy.go b/v2/pkg/assetserver/assetserver_legacy.go new file mode 100644 index 000000000..2d315aca3 --- /dev/null +++ b/v2/pkg/assetserver/assetserver_legacy.go @@ -0,0 +1,86 @@ +package assetserver + +import ( + "io" + "net/http" + + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +// 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) ProcessHTTPRequestLegacy(rw http.ResponseWriter, reqGetter func() (*http.Request, error)) { + d.processWebViewRequest(&legacyRequest{reqGetter: reqGetter, rw: rw}) +} + +type legacyRequest struct { + req *http.Request + rw http.ResponseWriter + + reqGetter func() (*http.Request, error) +} + +func (r *legacyRequest) URL() (string, error) { + req, err := r.request() + if err != nil { + return "", err + } + return req.URL.String(), nil +} + +func (r *legacyRequest) Method() (string, error) { + req, err := r.request() + if err != nil { + return "", err + } + return req.Method, nil +} + +func (r *legacyRequest) Header() (http.Header, error) { + req, err := r.request() + if err != nil { + return nil, err + } + return req.Header, nil +} + +func (r *legacyRequest) Body() (io.ReadCloser, error) { + req, err := r.request() + if err != nil { + return nil, err + } + return req.Body, nil +} + +func (r legacyRequest) Response() webview.ResponseWriter { + return &legacyRequestNoOpCloserResponseWriter{r.rw} +} + +func (r legacyRequest) AddRef() error { + return nil +} + +func (r legacyRequest) Release() error { + return nil +} + +func (r *legacyRequest) request() (*http.Request, error) { + if r.req != nil { + return r.req, nil + } + + req, err := r.reqGetter() + if err != nil { + return nil, err + } + r.req = req + return req, nil +} + +type legacyRequestNoOpCloserResponseWriter struct { + http.ResponseWriter +} + +func (*legacyRequestNoOpCloserResponseWriter) Finish() error { + return nil +} diff --git a/v2/pkg/assetserver/assetserver_webview.go b/v2/pkg/assetserver/assetserver_webview.go new file mode 100644 index 000000000..3a7178c20 --- /dev/null +++ b/v2/pkg/assetserver/assetserver_webview.go @@ -0,0 +1,163 @@ +package assetserver + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + + "github.com/wailsapp/wails/v2/pkg/assetserver/webview" +) + +type assetServerWebView struct { + // ExpectedWebViewHost is checked against the Request Host of every WebViewRequest, other hosts won't be processed. + ExpectedWebViewHost string + + dispatchInit sync.Once + dispatchReqC chan<- webview.Request + dispatchWorkers int +} + +// ServeWebViewRequest processes the HTTP Request asynchronously 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) ServeWebViewRequest(req webview.Request) { + d.dispatchInit.Do(func() { + workers := d.dispatchWorkers + if workers == 0 { + workers = 10 + } + + workerC := make(chan webview.Request, workers*2) + for i := 0; i < workers; i++ { + go func() { + for req := range workerC { + d.processWebViewRequest(req) + req.Release() + } + }() + } + + dispatchC := make(chan webview.Request) + go queueingDispatcher(50, dispatchC, workerC) + + d.dispatchReqC = dispatchC + }) + + if err := req.AddRef(); err != nil { + uri, _ := req.URL() + d.logError("Unable to call AddRef for request '%s'", uri) + return + } + + d.dispatchReqC <- req +} + +// 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) processWebViewRequest(r webview.Request) { + wrw := r.Response() + defer wrw.Finish() + + var rw http.ResponseWriter = &contentTypeSniffer{rw: wrw} // Make sure we have a Content-Type sniffer + defer rw.WriteHeader(http.StatusNotImplemented) // This is a NOP when a handler has already written and set the status + + uri, err := r.URL() + if err != nil { + d.logError("Error processing request, unable to get URL: %s (HttpResponse=500)", err) + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + method, err := r.Method() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Method: %w", err)) + return + } + + header, err := r.Header() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Header: %w", err)) + return + } + + body, err := r.Body() + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Body: %w", err)) + return + } + + if body == nil { + body = http.NoBody + } + defer body.Close() + + req, err := http.NewRequest(method, uri, body) + if err != nil { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("HTTP-Request: %w", err)) + return + } + req.Header = header + + 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 + } + + if expectedHost := d.ExpectedWebViewHost; expectedHost != "" && expectedHost != req.Host { + d.webviewRequestErrorHandler(uri, rw, fmt.Errorf("expected host '%s' in request, but was '%s'", expectedHost, req.Host)) + return + } + + d.ServeHTTP(rw, req) +} + +func (d *AssetServer) webviewRequestErrorHandler(uri string, rw http.ResponseWriter, err error) { + logInfo := uri + if uri, err := url.ParseRequestURI(uri); err == nil { + logInfo = strings.Replace(logInfo, fmt.Sprintf("%s://%s", uri.Scheme, uri.Host), "", 1) + } + + d.logError("Error processing request '%s': %s (HttpResponse=500)", logInfo, err) + http.Error(rw, err.Error(), http.StatusInternalServerError) +} + +func queueingDispatcher[T any](minQueueSize uint, inC <-chan T, outC chan<- T) { + q := newRingqueue[T](minQueueSize) + for { + in, ok := <-inC + if !ok { + return + } + + q.Add(in) + for q.Len() != 0 { + out, _ := q.Peek() + select { + case outC <- out: + q.Remove() + case in, ok := <-inC: + if !ok { + return + } + + q.Add(in) + } + } + } +} diff --git a/v2/pkg/assetserver/ringqueue.go b/v2/pkg/assetserver/ringqueue.go new file mode 100644 index 000000000..b94e7cd5c --- /dev/null +++ b/v2/pkg/assetserver/ringqueue.go @@ -0,0 +1,101 @@ +// Code from https://github.com/erikdubbelboer/ringqueue +/* +The MIT License (MIT) + +Copyright (c) 2015 Erik Dubbelboer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package assetserver + +type ringqueue[T any] struct { + nodes []T + head int + tail int + cnt int + + minSize int +} + +func newRingqueue[T any](minSize uint) *ringqueue[T] { + if minSize < 2 { + minSize = 2 + } + return &ringqueue[T]{ + nodes: make([]T, minSize), + minSize: int(minSize), + } +} + +func (q *ringqueue[T]) resize(n int) { + nodes := make([]T, n) + if q.head < q.tail { + copy(nodes, q.nodes[q.head:q.tail]) + } else { + copy(nodes, q.nodes[q.head:]) + copy(nodes[len(q.nodes)-q.head:], q.nodes[:q.tail]) + } + + q.tail = q.cnt % n + q.head = 0 + q.nodes = nodes +} + +func (q *ringqueue[T]) Add(i T) { + if q.cnt == len(q.nodes) { + // Also tested a grow rate of 1.5, see: http://stackoverflow.com/questions/2269063/buffer-growth-strategy + // In Go this resulted in a higher memory usage. + q.resize(q.cnt * 2) + } + q.nodes[q.tail] = i + q.tail = (q.tail + 1) % len(q.nodes) + q.cnt++ +} + +func (q *ringqueue[T]) Peek() (T, bool) { + if q.cnt == 0 { + var none T + return none, false + } + return q.nodes[q.head], true +} + +func (q *ringqueue[T]) Remove() (T, bool) { + if q.cnt == 0 { + var none T + return none, false + } + i := q.nodes[q.head] + q.head = (q.head + 1) % len(q.nodes) + q.cnt-- + + if n := len(q.nodes) / 2; n > q.minSize && q.cnt <= n { + q.resize(n) + } + + return i, true +} + +func (q *ringqueue[T]) Cap() int { + return cap(q.nodes) +} + +func (q *ringqueue[T]) Len() int { + return q.cnt +} diff --git a/v2/pkg/assetserver/webview/request.go b/v2/pkg/assetserver/webview/request.go new file mode 100644 index 000000000..b0ce3d069 --- /dev/null +++ b/v2/pkg/assetserver/webview/request.go @@ -0,0 +1,18 @@ +package webview + +import ( + "io" + "net/http" +) + +type Request interface { + URL() (string, error) + Method() (string, error) + Header() (http.Header, error) + Body() (io.ReadCloser, error) + + Response() ResponseWriter + + AddRef() error + Release() error +} diff --git a/v2/pkg/assetserver/webview/request_darwin.go b/v2/pkg/assetserver/webview/request_darwin.go new file mode 100644 index 000000000..4f4919fab --- /dev/null +++ b/v2/pkg/assetserver/webview/request_darwin.go @@ -0,0 +1,251 @@ +//go:build darwin + +package webview + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework WebKit + +#import +#import + +static void URLSchemeTaskRetain(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + [urlSchemeTask retain]; +} + +static void URLSchemeTaskRelease(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + [urlSchemeTask release]; +} + +static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + return [urlSchemeTask.request.URL.absoluteString UTF8String]; + } +} + +static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + return [urlSchemeTask.request.HTTPMethod UTF8String]; + } +} + +static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + NSData *headerData = [NSJSONSerialization dataWithJSONObject: urlSchemeTask.request.allHTTPHeaderFields options:0 error: nil]; + if (!headerData) { + return nil; + } + + NSString* headerString = [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] autorelease]; + const char * headerJSON = [headerString UTF8String]; + + char * headersOut = malloc(strlen(headerJSON)); + strcpy(headersOut, headerJSON); + return headersOut; + } +} + +static bool URLSchemeTaskRequestBodyBytes(void *wkUrlSchemeTask, const void **body, int *bodyLen) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBody) { + return false; + } + + *body = urlSchemeTask.request.HTTPBody.bytes; + *bodyLen = urlSchemeTask.request.HTTPBody.length; + return true; + } +} + +static bool URLSchemeTaskRequestBodyStreamOpen(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBodyStream) { + return false; + } + + [urlSchemeTask.request.HTTPBodyStream open]; + return true; + } +} + +static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) { + id urlSchemeTask = (id) wkUrlSchemeTask; + @autoreleasepool { + if (!urlSchemeTask.request.HTTPBodyStream) { + return; + } + + [urlSchemeTask.request.HTTPBodyStream close]; + } +} + +static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) { + id urlSchemeTask = (id) wkUrlSchemeTask; + + @autoreleasepool { + NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream; + if (!stream) { + return -2; + } + + NSStreamStatus status = stream.streamStatus; + if (status == NSStreamStatusAtEnd || !stream.hasBytesAvailable) { + return 0; + } else if (status != NSStreamStatusOpen) { + return -3; + } + + return [stream read:buf maxLength:bufLen]; + } +} +*/ +import "C" + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "unsafe" +) + +// NewRequest creates as new WebViewRequest based on a pointer to an `id` +// +// Please make sure to call Release() when finished using the request. +func NewRequest(wkURLSchemeTask unsafe.Pointer) Request { + C.URLSchemeTaskRetain(wkURLSchemeTask) + return &request{task: wkURLSchemeTask} +} + +var _ Request = &request{} + +type request struct { + task unsafe.Pointer + + header http.Header + body io.ReadCloser + rw *responseWriter +} + +func (r *request) AddRef() error { + C.URLSchemeTaskRetain(r.task) + return nil +} + +func (r *request) Release() error { + C.URLSchemeTaskRelease(r.task) + return nil +} + +func (r *request) URL() (string, error) { + return C.GoString(C.URLSchemeTaskRequestURL(r.task)), nil +} + +func (r *request) Method() (string, error) { + return C.GoString(C.URLSchemeTaskRequestMethod(r.task)), nil +} + +func (r *request) Header() (http.Header, error) { + if r.header != nil { + return r.header, nil + } + + header := http.Header{} + if cHeaders := C.URLSchemeTaskRequestHeadersJSON(r.task); cHeaders != nil { + if headers := C.GoString(cHeaders); headers != "" { + var h map[string]string + if err := json.Unmarshal([]byte(headers), &h); err != nil { + return nil, fmt.Errorf("unable to unmarshal request headers: %s", err) + } + + for k, v := range h { + header.Add(k, v) + } + } + C.free(unsafe.Pointer(cHeaders)) + } + r.header = header + return header, nil +} + +func (r *request) Body() (io.ReadCloser, error) { + if r.body != nil { + return r.body, nil + } + + var body unsafe.Pointer + var bodyLen C.int + if C.URLSchemeTaskRequestBodyBytes(r.task, &body, &bodyLen) { + if body != nil && bodyLen > 0 { + r.body = io.NopCloser(bytes.NewReader(C.GoBytes(body, bodyLen))) + } else { + r.body = http.NoBody + } + } else if C.URLSchemeTaskRequestBodyStreamOpen(r.task) { + r.body = &requestBodyStreamReader{task: r.task} + } + + return r.body, nil +} + +func (r *request) Response() ResponseWriter { + if r.rw != nil { + return r.rw + } + + r.rw = &responseWriter{r: r} + return r.rw +} + +var _ io.ReadCloser = &requestBodyStreamReader{} + +type requestBodyStreamReader struct { + task unsafe.Pointer + closed bool +} + +// Read implements io.Reader +func (r *requestBodyStreamReader) Read(p []byte) (n int, err error) { + var content unsafe.Pointer + var contentLen int + if p != nil { + content = unsafe.Pointer(&p[0]) + contentLen = len(p) + } + + res := C.URLSchemeTaskRequestBodyStreamRead(r.task, content, C.int(contentLen)) + if res > 0 { + return int(res), nil + } + + switch res { + case 0: + return 0, io.EOF + case -1: + return 0, fmt.Errorf("body: stream error") + case -2: + return 0, fmt.Errorf("body: no stream defined") + case -3: + return 0, io.ErrClosedPipe + default: + return 0, fmt.Errorf("body: unknown error %d", res) + } +} + +func (r *requestBodyStreamReader) Close() error { + if r.closed { + return nil + } + r.closed = true + + C.URLSchemeTaskRequestBodyStreamClose(r.task) + return nil +} diff --git a/v2/pkg/assetserver/webview/responsewriter.go b/v2/pkg/assetserver/webview/responsewriter.go new file mode 100644 index 000000000..c372275df --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter.go @@ -0,0 +1,14 @@ +package webview + +import ( + "net/http" +) + +// A ResponseWriter interface is used by an HTTP handler to +// construct an HTTP response for the WebView. +type ResponseWriter interface { + http.ResponseWriter + + // Finish the response and flush all data. + Finish() error +} diff --git a/v2/pkg/assetserver/webview/responsewriter_darwin.go b/v2/pkg/assetserver/webview/responsewriter_darwin.go new file mode 100644 index 000000000..1caa6e85d --- /dev/null +++ b/v2/pkg/assetserver/webview/responsewriter_darwin.go @@ -0,0 +1,154 @@ +//go:build darwin + +package webview + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework WebKit + +#import +#import + +typedef void (^schemeTaskCaller)(id); + +static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) { + id urlSchemeTask = (id) wkUrlSchemeTask; + if (urlSchemeTask == nil) { + return false; + } + + @autoreleasepool { + @try { + fn(urlSchemeTask); + } @catch (NSException *exception) { + // This is very bad to detect a stopped schemeTask this should be implemented in a better way + // But it seems to be very tricky to not deadlock when keeping a lock curing executing fn() + // It seems like those call switch the thread back to the main thread and then deadlocks when they reentrant want + // to get the lock again to start another request or stop it. + if ([exception.reason isEqualToString: @"This task has already been stopped"]) { + return false; + } + + @throw exception; + } + + return true; + } +} + +static bool URLSchemeTaskDidReceiveData(void *wkUrlSchemeTask, void* data, int datalength) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + NSData *nsdata = [NSData dataWithBytes:data length:datalength]; + [urlSchemeTask didReceiveData:nsdata]; + }); +} + +static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + [urlSchemeTask didFinish]; + }); +} + +static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) { + return urlSchemeTaskCall( + wkUrlSchemeTask, + ^(id urlSchemeTask) { + NSData *nsHeadersJSON = [NSData dataWithBytes:headersString length:headersStringLength]; + NSDictionary *headerFields = [NSJSONSerialization JSONObjectWithData:nsHeadersJSON options: NSJSONReadingMutableContainers error: nil]; + NSHTTPURLResponse *response = [[[NSHTTPURLResponse alloc] initWithURL:urlSchemeTask.request.URL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields] autorelease]; + + [urlSchemeTask didReceiveResponse:response]; + }); +} +*/ +import "C" + +import ( + "encoding/json" + "errors" + "net/http" + "unsafe" +) + +var ( + errRequestStopped = errors.New("request has been stopped") + errResponseFinished = errors.New("response has been finished") +) + +var _ ResponseWriter = &responseWriter{} + +type responseWriter struct { + r *request + + header http.Header + wroteHeader bool + + finished bool +} + +func (rw *responseWriter) Header() http.Header { + if rw.header == nil { + rw.header = http.Header{} + } + return rw.header +} + +func (rw *responseWriter) Write(buf []byte) (int, error) { + if rw.finished { + return 0, errResponseFinished + } + + rw.WriteHeader(http.StatusOK) + + var content unsafe.Pointer + var contentLen int + if buf != nil { + content = unsafe.Pointer(&buf[0]) + contentLen = len(buf) + } + + if !C.URLSchemeTaskDidReceiveData(rw.r.task, content, C.int(contentLen)) { + return 0, errRequestStopped + } + return contentLen, nil +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader || rw.finished { + return + } + rw.wroteHeader = true + + header := map[string]string{} + for k := range rw.Header() { + header[k] = rw.Header().Get(k) + } + headerData, _ := json.Marshal(header) + + var headers unsafe.Pointer + var headersLen int + if len(headerData) != 0 { + headers = unsafe.Pointer(&headerData[0]) + headersLen = len(headerData) + } + + C.URLSchemeTaskDidReceiveResponse(rw.r.task, C.int(code), headers, C.int(headersLen)) +} + +func (rw *responseWriter) Finish() error { + if !rw.wroteHeader { + rw.WriteHeader(http.StatusNotImplemented) + } + + if rw.finished { + return nil + } + rw.finished = true + + C.URLSchemeTaskDidFinish(rw.r.task) + return nil +}