[Go][Windows] Попробуйте WebView2 и CORS


Введение

Иногда я хочу автоматически сделать что-то на HTML и TypeScript.
Для этого я использую WebView2, как показано ниже.

  • [WPF][WebView2] Сохранить линейные графики как изображения

Можно ли сделать что-то подобное с помощью Go?

Среды

  • Go ver.go1.18.2 windows/amd64
  • WebView2 Runtime ver.101.0.1210.53

Использование WebView2

Я могу создавать GUI-приложения, используя технологии HTML.
Они условно делятся на два типа: один — для отображения HTML-файлов с помощью веб-браузера, а другой — для создания специализированного пользовательского интерфейса.

В этот раз я выбираю первый.

Для этого я могу использовать «jchv/go-webview2».

  • jchv/go-webview2 — GitHub

После установки WebView2 Runtime и выполнения «github.com/jchv/go-webview2», я просто пишу вот так.

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()
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Запуск локального сервера

Я мог показывать локальные HTML-файлы.

Но я не мог загружать файлы JavaScript или CSS.

Кроме того, некоторые API можно было использовать только на «http://localhost:XXXX» или «https://XXX», например MediaStream.

Поэтому я запускаю локальный сервер и WebView2 в главной функции.

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)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вызов Go из TypeScript

Возможно, потому что это очень простая библиотека, поэтому ее функций не слишком много.
Я могу вызывать функции Go из TypeScript следующим образом.

global.d.ts

declare function callGo();
Вход в полноэкранный режим Выйти из полноэкранного режима

main.page.ts

export function callGo() {
    window.callGo();
}
Вход в полноэкранный режим Выход из полноэкранного режима

main.go

...
    w.Bind("callGo", func() {
        log.Println("called")
    })
    w.Navigate("http://localhost:8082/")
    w.Run()
}
...
Войти в полноэкранный режим Выйти из полноэкранного режима

Но я не смог найти способ добавить некоторые аргументы.
И я также не смог найти способ вызова функций TypeScript(JavaScript).

Но поскольку они работают на локальном сервере, я могу отправлять веб-запросы и пакеты html/шаблонов 😛

  • go-webview2/main.go — jchv/go-webview2 — GitHub
  • webview/main.go — webview/webview — GitHub

CORS(Cross-Origin Resource Sharing)

Сначала я думал использовать WebView2 для отображения страницы WebRTC, которую я создал в прошлый раз.
Однако из-за того, что я получил ошибку CORS, я решил исправить ее.

По соображениям безопасности, если я отправляю запросы на кросс-оригинальный сайт со скриптами, который работает в моем веб-браузере, они должны иметь правильный заголовок CORS.

  • Кросс-оригинальный обмен ресурсами (CORS) — HTTP|MDN
  • 3.2. Протокол CORS — Fetch Standard

ASP.NET Core имеет встроенную функцию для стандартной обработки CORS.

  • Включение кросс-оригинальных запросов (CORS) в ASP.NET Core — Microsoft Docs

В Go также есть несколько библиотек для этого.

  • rs/cors — GitHub

Но я пытаюсь добавить CORS-заголовки самостоятельно.

GET, POST, PUT, DELETE, OPTIONS

Чтобы избежать ошибок CORS, я должен добавить некоторые заголовки в заголовок ответа.

Ошибка

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.
Вход в полноэкранный режим Выйти из полноэкранного режима

Сначала я попробовал установить все целевые источники в заголовке «Access-Control-Allow-Origin», как показано ниже.

[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", "*")
}
Вход в полноэкранный режим Выход из полноэкранного режима

[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)
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

[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));
}
Вход в полноэкранный режим Выход из полноэкранного режима

Но при отправке запросов я получаю ошибку.

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.
Вход в полноэкранный режим Выход из полноэкранного режима

Потому что заголовок «Access-Control-Allow-Origin» должен содержать одно значение.
Поэтому я изменил код.

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", "*")
}
Войти в полноэкранный режим Выйти из полноэкранного режима
  • 3. Расширения HTTP — Fetch Standard
  • Кросс-оригинальный обмен ресурсами (CORS) — HTTP|MDN

Preflight

Если веб-запрос не является так называемым «простым запросом», веб-браузер посылает «preflight request», чтобы проверить, можно ли его отправить до отправки запроса.

Чтобы управлять запросом preflight, не нужно ничего делать на стороне клиента и на стороне сервера.

  • Preflight request — MDN Web Docs Glossary: Определения терминов, связанных с веб|MDN

CORS-безопасный метод

Если я не добавляю «PUT», «DELETE», «OPTIONS» в заголовок «Access-Control-Allow-Methods», я могу блокировать их запросы.

Но я не могу блокировать запросы «GET», «POST» и «HEAD».
Потому что они являются так называемыми «CORS-запрещенными методами».

Поэтому даже если я установлю заголовок «Access-Control-Allow-Methods» как «PUT, DELETE, OPTIONS», я все равно могу отправлять запросы «GET» или «POST».

Запросы «preflight» тоже не блокируются.

  • Блокируют ли браузеры запросы POST, если POST не указан в значении Access-Control-Allow-Methods ответа preflight OPTIONS? — StackOverflow
  • 4.8. CORS-предупредительная выборка — Стандарт выборки

WebSocket

Как насчет WebSocket?

  • [Golang] Попробуйте WebSocket

Если запрос на подключение WebSocket нарушает CORS, в «upgrader.Upgrade» из «room.go» возникнет ошибка.

websocket: request origin not allowed by Upgrader.CheckOrigin
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтобы избежать этого, я должен реализовать функцию «CheckOrigin».

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

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

...
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();
}
Вход в полноэкранный режим Выход из полноэкранного режима
  • Как добавить доверенное происхождение в CheckOrigin gorilla websocket? — StackOverflow
  • пакет websocket — github.com/gorilla/websocket — pkg.go.dev

Оцените статью
Procodings.ru
Добавить комментарий