5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 04:42:00 +08:00

[assetServer, darwin] Use AssetServer native WKWebView request handling (#2283)

This commit is contained in:
stffabi 2023-01-16 13:02:18 +01:00 committed by GitHub
parent 8f92cf1074
commit caa0b6c804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 808 additions and 455 deletions

View File

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

View File

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

View File

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

View File

@ -108,7 +108,6 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
[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<WKURLSchemeTask>);
}
- (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,158 +427,21 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
}];
}
- (void) processURLDidReceiveResponse:(unsigned long long)requestId :(int)statusCode :(NSData *)headersJSON {
[self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> 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<WKURLSchemeTask> urlSchemeTask) {
[urlSchemeTask didReceiveData:data];
}];
}
- (void) processURLDidFinish:(unsigned long long)requestId {
[self processURLSchemeTaskCall:requestId :^(id<WKURLSchemeTask> 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<WKURLSchemeTask> 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<WKURLSchemeTask>)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<WKURLSchemeTask>)urlSchemeTask {
NSArray<NSNumber*> *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<WKURLSchemeTask> urlSchemeTask = nil;
@synchronized(self.urlRequests) {
urlSchemeTask = self.urlRequests[urlSchemeTaskKey];
}
if (!urlSchemeTask) {
return;
}
NSInputStream *stream = urlSchemeTask.request.HTTPBodyStream;
if (stream) {
NSStreamStatus status = stream.streamStatus;
if (status != NSStreamStatusClosed && status != NSStreamStatusNotOpen) {
[stream close];
}
@synchronized(self.urlRequests) {
[self.urlRequests removeObjectForKey:urlSchemeTaskKey];
}
}
- (bool)processURLSchemeTaskCall:(unsigned long long)requestId :(schemeTaskCaller)fn {
NSNumber *key = [NSNumber numberWithUnsignedLongLong:requestId];
id<WKURLSchemeTask> 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;
}
@throw exception;
}
return true;
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
processMessage("DomReady");
}

View File

@ -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,13 +94,11 @@ 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.startMessageProcessor()
go result.startCallbackProcessor()
@ -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)
}

View File

@ -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*);

View File

@ -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 <stdlib.h>
*/
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)
}
}

View File

@ -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 <stdlib.h>
*/
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,251 @@
//go:build darwin
package webview
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework WebKit
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
static void URLSchemeTaskRetain(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
[urlSchemeTask retain];
}
static void URLSchemeTaskRelease(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
[urlSchemeTask release];
}
static const char * URLSchemeTaskRequestURL(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.URL.absoluteString UTF8String];
}
}
static const char * URLSchemeTaskRequestMethod(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
return [urlSchemeTask.request.HTTPMethod UTF8String];
}
}
static const char * URLSchemeTaskRequestHeadersJSON(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return false;
}
[urlSchemeTask.request.HTTPBodyStream open];
return true;
}
}
static void URLSchemeTaskRequestBodyStreamClose(void *wkUrlSchemeTask) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) wkUrlSchemeTask;
@autoreleasepool {
if (!urlSchemeTask.request.HTTPBodyStream) {
return;
}
[urlSchemeTask.request.HTTPBodyStream close];
}
}
static int URLSchemeTaskRequestBodyStreamRead(void *wkUrlSchemeTask, void *buf, int bufLen) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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<WKURLSchemeTask>`
//
// 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
}

View File

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

View File

@ -0,0 +1,154 @@
//go:build darwin
package webview
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework WebKit
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
static bool urlSchemeTaskCall(void *wkUrlSchemeTask, schemeTaskCaller fn) {
id<WKURLSchemeTask> urlSchemeTask = (id<WKURLSchemeTask>) 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<WKURLSchemeTask> urlSchemeTask) {
NSData *nsdata = [NSData dataWithBytes:data length:datalength];
[urlSchemeTask didReceiveData:nsdata];
});
}
static bool URLSchemeTaskDidFinish(void *wkUrlSchemeTask) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> urlSchemeTask) {
[urlSchemeTask didFinish];
});
}
static bool URLSchemeTaskDidReceiveResponse(void *wkUrlSchemeTask, int statusCode, void *headersString, int headersStringLength) {
return urlSchemeTaskCall(
wkUrlSchemeTask,
^(id<WKURLSchemeTask> 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
}