[Go][Windows] Try WebView2 and CORS
Tags: go
Published: 2022-05-31 17:04:34

## Intro
Sometimes I want to automatically do something on HTML and TypeScript.
Thus I use WebView2 like below.

* [[WPF][WebView2] Save line charts as images](https://dev.to/masanori_msl/wpfwebview2-save-line-charts-as-images-5ana)

Is it possible to do something similar with Go?

### Environments
* Go ver.go1.18.2 windows/amd64
* WebView2 Runtime ver.101.0.1210.53 

## Using WebView2
I can create GUI applications by using HTML technologies.
They are roughly divided into two types, one is to display HTML files with web browser, and the other is to create dedicated UI.

I choose the first one in this time.

To do this, I can use "jchv/go-webview2".

* [jchv/go-webview2 - GitHub](https://github.com/jchv/go-webview2)

After installing [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) and executing "github.com/jchv/go-webview2", I just write like this.

#### main.go
```go
package main

import (
	"log"
	"os"
	"path/filepath"

	"github.com/jchv/go-webview2"
)

func main() {
	w := webview2.NewWithOptions(webview2.WebViewOptions{
		Debug:     true, // To display the development tools
		AutoFocus: true,
		WindowOptions: webview2.WindowOptions{
			Title:  "webview example",
			Width:  200,
			Height: 200,
			IconId: 2, // icon resource id
			Center: true,
		},
	})
	if w == nil {
		log.Fatalln("Failed to load webview.")
	}
	defer w.Destroy()
	// The window will be displayed once and then resized to this size.
	w.SetSize(600, 600, webview2.HintNone)
	// load a local HTML file.
	c, err := os.Getwd()
	if err != nil {
		log.Fatalln(err.Error())
	}
	w.Navigate(filepath.Join(c, "templates/index.html"))
	w.Run()
}
```

## Launch a local server
I could show local HTML files.

But I couldn't load JavaScript or CSS files.

In addition to that, some APIs only could be used on "http://localhost:XXXX" or "https://XXX" like MediaStream.

So I launch a local server and WebView2 in the main function.

#### main.go
```go
package main

import (
	"html/template"
	"log"
	"net/http"
	"sync"

	"github.com/jchv/go-webview2"
)

type templateHandler struct {
	once     sync.Once
	filename string
	templ    *template.Template
}

func main() {
	w := webview2.NewWithOptions(webview2.WebViewOptions{
		Debug:     true, // To display the development tools
		AutoFocus: true,
		WindowOptions: webview2.WindowOptions{
			Title:  "webview example",
			Width:  200,
			Height: 200,
			IconId: 2, // icon resource id
			Center: true,
		},
	})
	if w == nil {
		log.Fatalln("Failed to load webview.")
	}
	defer w.Destroy()
	// Launch a local web server on another goroutine
	go handleHTTPRequest()
	
	w.SetSize(600, 600, webview2.HintNone)
	w.Navigate("http://localhost:8082/")
	w.Run()
}
func handleHTTPRequest() {
	http.Handle("/css/", http.FileServer(http.Dir("templates")))
	http.Handle("/js/", http.FileServer(http.Dir("templates")))
	http.Handle("/", &templateHandler{filename: "index.html"})
	log.Fatal(http.ListenAndServe("localhost:8082", nil))
}
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	t.once.Do(func() {
		t.templ = template.Must(template.ParseFiles(filepath.Join("templates", t.filename)))
	})
	t.templ.Execute(w, nil)
}
```

### Call Go from TypeScript
Probably because this is a greate simple library, so its functions aren't too much.
I can call Go functions from TypeScript like this.

#### global.d.ts
```ts
declare function callGo();
```

#### main.page.ts
```ts
export function callGo() {
    window.callGo();
}
```

#### main.go
```go
...
	w.Bind("callGo", func() {
		log.Println("called")
	})
	w.Navigate("http://localhost:8082/")
	w.Run()
}
...
```

But I couldn't find a way to add some arguments.
And I also couldn't find a way to call TypeScript(JavaScript) functions.

But because they works on the local server, I can send web requests and html/template packages :P

* [go-webview2/main.go - jchv/go-webview2 - GitHub](https://github.com/jchv/go-webview2/blob/master/cmd/demo/main.go)
* [webview/main.go - webview/webview - GitHub](https://github.com/webview/webview/blob/master/examples/scrape/main.go)

## CORS(Cross-Origin Resource Sharing)
First, I was thinking of using WebView2 to display the WebRTC page I created last time.
However, because I got a CORS error, so I decided to fix it.

For security reasons, if I send requests to a cross-origin site with scripts that runs on my web browser, they must have the correct CORS header.

* [Cross-Origin Resource Sharing (CORS) - HTTP|MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
* [3.2. CORS protocol - Fetch Standard](https://fetch.spec.whatwg.org/#http-cors-protocol)

ASP.NET Core has a built-in function to handle CORS as standard.

* [Enable Cross-Origin Requests (CORS) in ASP.NET Core - Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-6.0)

And Go also has some libraries to do that.

* [rs/cors - GitHub](https://github.com/rs/cors)

But I try adding CORS headers by myself.

### GET, POST, PUT, DELETE, OPTIONS
To avoid CORS errors, I have to add some headers into the response header.

#### Error
```
Access to fetch at 'http://localhost:8083/req' from origin 'http://localhost:8082' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

First, I tried set all target origins into "Access-Control-Allow-Origin" header like below.

#### [Server-Side] trustedCors.go (Failed)
```go
package main

import (
	"log"
	"net/http"
)

func SetCORS(w *http.ResponseWriter) {
	(*w).Header().Set("Access-Control-Allow-Origin", "http://localhost:8082, http://localhost:8083")
	(*w).Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
	(*w).Header().Set("Access-Control-Allow-Headers", "*")
}
```

#### [Server-Side] webrequestHandler.go
```go
package main

import (
	"fmt"
	"net/http"
)

func HandleWebrequest(w http.ResponseWriter, r *http.Request) {
	SetCORS(&w)
	switch r.Method {
	case http.MethodGet:
		fmt.Fprintln(w, "GET REQUEST")
	case http.MethodPost:
		fmt.Fprintln(w, "POST REQUEST")
	case http.MethodPut:
		fmt.Fprintln(w, "PUT REQUEST")
	case http.MethodDelete:
		fmt.Fprintln(w, "DELETE REQUEST")
	default:
		fmt.Fprintf(w, "%s REQUEST", r.Method)
	}
}
```

#### [Client-Side] main.page.ts
```ts
export function sendGetRequest() {
    fetch("http://localhost:8083/req", 
        {
            method: "GET",
        })
        .then(res => res.text())
        .then(txt => console.log(txt))
        .catch(err => console.error(err));
}
export function sendPostRequest() {
    fetch("http://localhost:8083/req",
        {
            method: "POST",
            body: JSON.stringify({ messageType: "text", data: "Hello" }),
        })
        .then(res => res.text())
        .then(txt => console.log(txt))
        .catch(err => console.error(err));
}
export function sendPutRequest() {
    fetch("http://localhost:8083/req", 
        {
            method: "PUT",
        })
        .then(res => res.text())
        .then(txt => console.log(txt))
        .catch(err => console.error(err));
}
export function sendDeleteRequest() {
    fetch("http://localhost:8083/req", 
        {
            method: "DELETE",
        })
        .then(res => res.text())
        .then(txt => console.log(txt))
        .catch(err => console.error(err));
}
export function sendOptionsRequest() {
    fetch("http://localhost:8083/req", 
        {
            method: "OPTIONS",
        })
        .then(res => res.text())
        .then(txt => console.log(txt))
        .catch(err => console.error(err));
}
```

But I got an error when I sent requests.

```
Access to fetch at 'http://localhost:8083/req' from origin 'http://localhost:8082' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:8082, http://localhost:8083', but only one is allowed. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```

Because "Access-Control-Allow-Origin" header must contain single value.
So I changed the code.

#### trustedCors.go (OK)
```go
var (
	trustedOrigins = []string{"http://localhost:8082", "http://localhost:8083"}
)

func SetCORS(w *http.ResponseWriter, r *http.Request) {
	origin := r.Header.Get("Origin")
	targetOrigin := ""
	for _, o := range trustedOrigins {
		if origin == o {
			targetOrigin = o
			break
		}
	}
	(*w).Header().Set("Access-Control-Allow-Origin", targetOrigin)
	(*w).Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
	(*w).Header().Set("Access-Control-Allow-Headers", "*")
}
```

* [3. HTTP extensions - Fetch Standard](https://fetch.spec.whatwg.org/#http-extensions)
* [Cross-Origin Resource Sharing (CORS) - HTTP|MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)

### Preflight
If the web request is not a so-called ["simple requests"](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests), the web browser sends a "preflight request" to see if it can be sent before sending the request.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t315b32catwl9qotzbz3.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0usfkwaw75ntjdw16zjf.png)

To control preflight request, I don't need do anything on the client-side and server-side.

* [Preflight request - MDN Web Docs Glossary: Definitions of Web-related terms|MDN](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request)

### CORS-safelisted method
If I don't add "PUT", "DELETE", "OPTIONS" into the "Access-Control-Allow-Methods" header, I can block their requests.

But I can't block "GET", "POST" and "HEAD" requests.
Because they are so-called "CORS-safelisted method".

So even if I set "Access-Control-Allow-Methods" header as "PUT, DELETE, OPTIONS", I still can send "GET" or "POST" requests.

The "preflight" requests aren't blocked, too.

* [Do browsers block POST requests if POST isn’t in the Access-Control-Allow-Methods value of the preflight OPTIONS response? - StackOverflow](https://stackoverflow.com/questions/71409753/do-browsers-block-post-requests-if-post-isn-t-in-the-access-control-allow-method)
* [4.8. CORS-preflight fetch - Fetch Standard](https://fetch.spec.whatwg.org/#cors-preflight-fetch)

### WebSocket
How about WebSocket?

* [[Golang] Try WebSocket](https://dev.to/masanori_msl/golang-try-websocket-1ghm)

If a request for connecting WebSocket violates CORS, an error will be occurred in "upgrader.Upgrade" of "room.go".

```
websocket: request origin not allowed by Upgrader.CheckOrigin
```

To avoid this, I have to implement the "CheckOrigin" function.

#### trustedCors.go
```go
...
func ValidateCORS(r *http.Request) bool {
	origin := r.Header.Get("Origin")
	for _, o := range trustedOrigins {
		if o == origin {
			return true
		}
	}
	return false
}
```

#### [Server-Side] websocketHandler.go
```go
package main

import (
	"encoding/json"
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return ValidateCORS(r)
	},
}

type websocketMessage struct {
	MessageType string `json:"messageType"`
	Data        string `json:"data"`
}

func HandleWebsocket(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	// Close the connection when the for-loop operation is finished.
	defer conn.Close()

	message := &websocketMessage{}
	for {
		messageType, raw, err := conn.ReadMessage()
		if err != nil {
			log.Println(err)
			return
		} else if err := json.Unmarshal(raw, &message); err != nil {
			log.Println(err)
			return
		}
		log.Println(messageType)
		conn.WriteJSON(message)
	}
}
```

#### [Client-Side] main.page.ts
```ts
...
let ws: WebSocket|null = null;
export function connectWs() {
    ws = new WebSocket("ws://localhost:8083/ws");
    ws.onmessage = data => {
        const message = JSON.parse(data.data);
        console.log(message);
    };
}
export function closeWs() {
    ws?.close();
}
```

* [How to add a trusted origin to gorilla websocket's CheckOrigin? - StackOverflow](https://stackoverflow.com/questions/65034144/how-to-add-a-trusted-origin-to-gorilla-websockets-checkorigin)
* [websocket package - github.com/gorilla/websocket - pkg.go.dev](https://pkg.go.dev/github.com/gorilla/websocket#Upgrader.CheckOrigin)