DEV Community

Masui Masanori
Masui Masanori

Posted on

[Go][Windows] Try WebView2 and CORS

#go

Intro

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

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".

After installing WebView2 Runtime and executing "github.com/jchv/go-webview2", I just write like this.

main.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()
}
Enter fullscreen mode Exit fullscreen mode

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

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)
}
Enter fullscreen mode Exit fullscreen mode

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

declare function callGo();
Enter fullscreen mode Exit fullscreen mode

main.page.ts

export function callGo() {
    window.callGo();
}
Enter fullscreen mode Exit fullscreen mode

main.go

...
    w.Bind("callGo", func() {
        log.Println("called")
    })
    w.Navigate("http://localhost:8082/")
    w.Run()
}
...
Enter fullscreen mode Exit fullscreen mode

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

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.

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

And Go also has some libraries to do that.

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.
Enter fullscreen mode Exit fullscreen mode

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

[Server-Side] trustedCors.go (Failed)

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", "*")
}
Enter fullscreen mode Exit fullscreen mode

[Server-Side] webrequestHandler.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)
    }
}
Enter fullscreen mode Exit fullscreen mode

[Client-Side] main.page.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));
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

trustedCors.go (OK)

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", "*")
}
Enter fullscreen mode Exit fullscreen mode

Preflight

If the web request is not a so-called "simple requests", the web browser sends a "preflight request" to see if it can be sent before sending the request.

Image description

Image description

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

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.

WebSocket

How about WebSocket?

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
Enter fullscreen mode Exit fullscreen mode

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

trustedCors.go

...
func ValidateCORS(r *http.Request) bool {
    origin := r.Header.Get("Origin")
    for _, o := range trustedOrigins {
        if o == origin {
            return true
        }
    }
    return false
}
Enter fullscreen mode Exit fullscreen mode

[Server-Side] websocketHandler.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)
    }
}
Enter fullscreen mode Exit fullscreen mode

[Client-Side] main.page.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();
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)