mirror of
https://gitee.com/energye/energy.git
synced 2024-12-02 11:47:37 +08:00
417 lines
12 KiB
Go
417 lines
12 KiB
Go
//----------------------------------------
|
||
//
|
||
// Copyright © yanghy. All Rights Reserved.
|
||
//
|
||
// Licensed under Apache License Version 2.0, January 2004
|
||
//
|
||
// https://www.apache.org/licenses/LICENSE-2.0
|
||
//
|
||
//----------------------------------------
|
||
|
||
package cef
|
||
|
||
import (
|
||
"bytes"
|
||
"embed"
|
||
"github.com/energye/energy/v2/cef/process"
|
||
. "github.com/energye/energy/v2/consts"
|
||
"github.com/energye/energy/v2/logger"
|
||
"io/ioutil"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"reflect"
|
||
"unsafe"
|
||
)
|
||
|
||
const (
|
||
localDomain = "energy" // 默认本地资源加载域
|
||
localProto = "fs" // 默认本地资源加载协议
|
||
localHome = "/index.html" //
|
||
)
|
||
|
||
// 本地加载资源
|
||
var localLoadRes *LocalLoadResource
|
||
|
||
// LocalLoadResource 初始化时设置
|
||
// 本地&内置加载资源
|
||
type LocalLoadResource struct {
|
||
LocalLoadConfig
|
||
mimeType map[string]string
|
||
sourceCache map[string]*source
|
||
}
|
||
|
||
// LocalLoadConfig
|
||
// 本地&内置资源加载配置
|
||
// 然后使用 Build() 函数构建对象
|
||
type LocalLoadConfig struct {
|
||
Enable bool // 设置是否启用本地资源缓存到内存, 默认true: 启用, 禁用时需要调用Disable函数
|
||
Domain string // 自定义域, 格式: xxx | xxx.xx | xxx.xxx.xxx, example, example.com, 默认: energy
|
||
Scheme string // 自定义协议, 不建议使用 HTTP、HTTPS、FILE、FTP、ABOUT和DATA 默认: fs
|
||
// 资源根目录, fs为空时: 本地目录(默认当前程序执行目录), fs不为空时: 默认值 resources, 使用内置加载
|
||
// 本地目录规则: 空("")时当前目录, @当前目录开始(@/to/path),或绝对目录.
|
||
ResRootDir string //
|
||
FS *embed.FS // 内置加载资源对象, 不为nil时使用内置加载,默认: nil
|
||
Proxy IXHRProxy // 数据请求代理, 在浏览器发送xhr请求时可通过该配置转发, 你可自定义实现该 IXHRProxy 接口
|
||
Home string // 默认首页HTML文件名: /index.html , 默认: /index.html
|
||
exePath string // 执行文件当前目录
|
||
}
|
||
|
||
// 请求和响应资源
|
||
type source struct {
|
||
path string // 资源路径, 根据请求URL地址
|
||
fileExt string // 资源扩展名, 用于拿到 MimeType
|
||
bytes []byte // 资源数据
|
||
err error // 获取资源时的错误
|
||
start int // 读取资源时的地址偏移
|
||
statusCode int32 // 响应状态码
|
||
statusText string // 响应状态文本
|
||
mimeType string // 响应的资源 MimeType
|
||
header map[string][]string // 响应头
|
||
resourceType TCefResourceType // 资源类型
|
||
}
|
||
|
||
// 初始化本地加载配置对象
|
||
func localLoadResourceInit(config *LocalLoadConfig) {
|
||
if config == nil && !process.Args.IsMain() {
|
||
return
|
||
}
|
||
localLoadRes = &LocalLoadResource{
|
||
mimeType: make(map[string]string),
|
||
sourceCache: make(map[string]*source),
|
||
}
|
||
localLoadRes.LocalLoadConfig = *config
|
||
}
|
||
|
||
// Build
|
||
// 构建本地资源加载配置
|
||
// 初始化默认值和默认代理配置
|
||
func (m LocalLoadConfig) Build() *LocalLoadConfig {
|
||
if localLoadRes != nil && !process.Args.IsMain() {
|
||
return nil
|
||
}
|
||
var config = &m
|
||
config.Enable = true
|
||
// domain 必须设置
|
||
if config.Domain == "" {
|
||
config.Domain = localDomain
|
||
}
|
||
if config.Scheme == "" {
|
||
config.Scheme = localProto
|
||
}
|
||
// 默认使用 /index.html
|
||
if config.Home == "" {
|
||
config.Home = localHome
|
||
} else if config.Home[0] != '/' {
|
||
config.Home = "/" + config.Home
|
||
}
|
||
m.exePath = ExeDir
|
||
// 默认的资源目录
|
||
if config.ResRootDir == "" {
|
||
if config.FS != nil {
|
||
config.ResRootDir = "resources"
|
||
} else {
|
||
config.ResRootDir = m.exePath
|
||
}
|
||
}
|
||
if m.Proxy != nil {
|
||
if proxy, ok := m.Proxy.(*XHRProxy); ok {
|
||
proxy.init()
|
||
}
|
||
}
|
||
return config
|
||
}
|
||
|
||
// Disable
|
||
// 如果不想启用该代理配置,需要主动调用该函数,仅在应用出始化时有效
|
||
func (m *LocalLoadConfig) Disable() *LocalLoadConfig {
|
||
m.Enable = false
|
||
return m
|
||
}
|
||
|
||
func (m *LocalLoadResource) loadDefaultURL(window IBrowserWindow, browser *ICefBrowser) {
|
||
if localLoadRes.enable() {
|
||
var homeURL string
|
||
if BrowserWindow.Config.Url != defaultAboutBlank {
|
||
homeURL = window.WindowProperty().Url
|
||
} else {
|
||
defaultURL := new(bytes.Buffer)
|
||
defaultURL.WriteString(m.Scheme)
|
||
defaultURL.WriteString("://")
|
||
defaultURL.WriteString(m.Domain)
|
||
if m.Home != localHome {
|
||
defaultURL.WriteString(m.Home)
|
||
}
|
||
homeURL = defaultURL.String()
|
||
}
|
||
window.Chromium().LoadUrl(homeURL)
|
||
}
|
||
}
|
||
|
||
// getSchemeHandlerFactory
|
||
// 方式二 资源处理器默认实现,使用本地资源加载时开启
|
||
func (m *LocalLoadResource) getSchemeHandlerFactory(browserWindow IBrowserWindow, browser *ICefBrowser) {
|
||
if localLoadRes.enable() {
|
||
//if reqUrl, err := url.Parse(request.URL()); err == nil && reqUrl.Scheme == string(localLoadRes.Scheme) {
|
||
handler := SchemeHandlerFactoryRef.New()
|
||
handler.SetNew(func(browser *ICefBrowser, frame *ICefFrame, schemeName string, request *ICefRequest) *ICefResourceHandler {
|
||
return m.getResourceHandler(browser, frame, request)
|
||
})
|
||
browser.GetRequestContext().RegisterSchemeHandlerFactory(localLoadRes.Scheme, localLoadRes.Domain, handler)
|
||
//browserWindow.setSchemeHandlerFactory(handler) // TODO
|
||
//}
|
||
}
|
||
return
|
||
}
|
||
|
||
// getResourceHandler
|
||
// 方式一 资源处理器默认实现,使用本地资源加载时开启
|
||
func (m *LocalLoadResource) getResourceHandler(browser *ICefBrowser, frame *ICefFrame, request *ICefRequest) *ICefResourceHandler {
|
||
if localLoadRes.enable() {
|
||
if source, ok := localLoadRes.checkRequest(request); ok {
|
||
handler := ResourceHandlerRef.New(browser, frame, localLoadRes.Scheme, request)
|
||
//handler.Open(source.open)
|
||
handler.ProcessRequest(source.processRequest)
|
||
handler.GetResponseHeaders(source.response)
|
||
//handler.Read(source.read)
|
||
handler.ReadResponse(source.readResponse)
|
||
return handler
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (m *LocalLoadResource) enable() bool {
|
||
if m == nil {
|
||
return false
|
||
}
|
||
return m.LocalLoadConfig.Enable
|
||
}
|
||
|
||
// getMimeType
|
||
// 获取资源mime type
|
||
func (m *LocalLoadResource) getMimeType(extension string) string {
|
||
if mimeType, ok := m.mimeType[extension]; ok {
|
||
return mimeType
|
||
} else {
|
||
mimeType = GetMimeType(extension)
|
||
m.mimeType[extension] = mimeType
|
||
return mimeType
|
||
}
|
||
}
|
||
|
||
// return xxx.js = js, xxx.html = html
|
||
func (m *LocalLoadResource) ext(path string) string {
|
||
for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- {
|
||
if path[i] == '.' {
|
||
return path[i+1:]
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// 使用本地资源加载时,先验证每个请求的合法性
|
||
// 所支持的scheme, domain
|
||
// URL格式, fs://energy/index.html, 文件路径必须包含扩展名
|
||
// 这里返回false后不会创建资源处理对象
|
||
func (m *LocalLoadResource) checkRequest(request *ICefRequest) (*source, bool) {
|
||
rt := request.ResourceType()
|
||
// 根据资源类型跳过哪些资源不被本地加载
|
||
// TODO: rt_media 类型应该在此去除
|
||
switch rt {
|
||
case RT_MEDIA, RT_PING, RT_CSP_REPORT, RT_PLUGIN_RESOURCE:
|
||
return nil, false
|
||
}
|
||
reqUrl, err := url.Parse(request.URL())
|
||
logger.Debug("LocalLoadResource URL:", reqUrl.String(), "RT:", rt)
|
||
if err != nil {
|
||
logger.Error("LocalLoadResource, scheme invalid should file")
|
||
return nil, false
|
||
}
|
||
if reqUrl.Scheme != m.Scheme {
|
||
logger.Error("LocalLoadResource, scheme invalid should file", "current:", reqUrl.Scheme)
|
||
return nil, false
|
||
}
|
||
if reqUrl.Host != m.Domain {
|
||
logger.Error("LocalLoadResource, Incorrect protocol domain should: [fs | file]://energy/index.html", "current:", reqUrl.Host)
|
||
return nil, false
|
||
}
|
||
path := reqUrl.Path
|
||
if path == "" || path == "/" {
|
||
path = m.Home
|
||
}
|
||
ext := m.ext(path)
|
||
return &source{path: path, fileExt: ext, mimeType: m.getMimeType(ext), resourceType: rt}, true
|
||
}
|
||
|
||
// 读取本地或内置资源
|
||
func (m *source) readFile() {
|
||
// 必须设置文件根目录, scheme是file时, fileRoot为本地文件目录, scheme是fs时, fileRoot为fs的目录名
|
||
if localLoadRes.FS == nil {
|
||
var path string
|
||
if localLoadRes.ResRootDir[0] == '@' {
|
||
//当前路径
|
||
path = filepath.Join(localLoadRes.exePath, localLoadRes.ResRootDir[1:])
|
||
} else {
|
||
//绝对路径
|
||
path = localLoadRes.ResRootDir
|
||
}
|
||
m.bytes, m.err = ioutil.ReadFile(filepath.Join(path, m.path))
|
||
// 在本地读取
|
||
if m.err != nil {
|
||
logger.Error("ReadFile:", m.err.Error())
|
||
}
|
||
} else {
|
||
//在fs读取
|
||
m.bytes, m.err = localLoadRes.FS.ReadFile(localLoadRes.ResRootDir + m.path)
|
||
if m.err != nil {
|
||
logger.Error("ReadFile:", m.err.Error())
|
||
}
|
||
}
|
||
}
|
||
|
||
// checkRequest = true, 打开资源
|
||
func (m *source) open(request *ICefRequest, callback *ICefCallback) (handleRequest, ok bool) {
|
||
m.start = 0
|
||
// 当前资源的响应设置默认值
|
||
m.statusCode = 404
|
||
m.statusText = "Not Found"
|
||
m.err = nil
|
||
m.header = nil
|
||
// xhr 请求, 需要通过代理转发出去
|
||
if m.resourceType == RT_XHR && localLoadRes.Proxy != nil {
|
||
if result, err := localLoadRes.Proxy.Send(request); err == nil {
|
||
m.bytes, m.err = result.Data, err
|
||
m.statusCode = result.StatusCode
|
||
m.statusText = result.Status
|
||
m.header = result.Header
|
||
} else {
|
||
m.err = err
|
||
m.statusText = err.Error()
|
||
}
|
||
} else {
|
||
m.readFile()
|
||
if m.err == nil {
|
||
m.statusCode = 200
|
||
m.statusText = "OK"
|
||
} else if localLoadRes.Proxy != nil {
|
||
// 尝试在代理服务请求资源
|
||
if result, err := localLoadRes.Proxy.Send(request); err == nil {
|
||
m.bytes, m.err = result.Data, err
|
||
m.statusCode = result.StatusCode
|
||
m.statusText = result.Status
|
||
m.header = result.Header
|
||
// TODO 需要验证 Content-Type 合法性
|
||
if ct, ok := result.Header["Content-Type"]; ok {
|
||
m.mimeType = ct[0]
|
||
} else {
|
||
m.mimeType = "text/html"
|
||
}
|
||
} else {
|
||
m.bytes = []byte("Invalid resource request")
|
||
m.mimeType = "application/json"
|
||
m.err = err
|
||
m.statusText = err.Error()
|
||
}
|
||
}
|
||
}
|
||
callback.Cont()
|
||
return true, true
|
||
}
|
||
|
||
func (m *source) processRequest(request *ICefRequest, callback *ICefCallback) bool {
|
||
_, _ = m.open(request, callback)
|
||
return true
|
||
}
|
||
|
||
// checkRequest = true, 设置响应信息
|
||
func (m *source) response(response *ICefResponse) (responseLength int64, redirectUrl string) {
|
||
response.SetStatus(m.statusCode)
|
||
response.SetStatusText(m.statusText)
|
||
response.SetMimeType(m.mimeType)
|
||
responseLength = int64(len(m.bytes))
|
||
if m.header != nil {
|
||
header := response.GetHeaderMap() //StringMultiMapRef.New()
|
||
if header.IsValid() {
|
||
for key, value := range m.header {
|
||
for _, vs := range value {
|
||
header.Append(key, vs)
|
||
}
|
||
}
|
||
response.SetHeaderMap(header)
|
||
}
|
||
header.Free()
|
||
}
|
||
return
|
||
}
|
||
|
||
//func (m *source) out(dataOut uintptr, bytesToRead int32) (bytesRead int32, result bool) {
|
||
// if m.bytes != nil {
|
||
// for bytesRead < bytesToRead && m.readPosition < len(m.bytes) {
|
||
// *(*byte)(unsafe.Pointer(dataOut + uintptr(bytesRead))) = m.bytes[m.readPosition]
|
||
// m.readPosition++
|
||
// bytesRead++
|
||
// }
|
||
// return bytesRead, bytesRead > 0
|
||
// }
|
||
// return
|
||
//}
|
||
|
||
func (m *source) out(dataOut uintptr, bytesToRead int32) (bytesRead int32, result bool) {
|
||
dataSize := len(m.bytes)
|
||
// start 当前读取的开始位置
|
||
// bytes 是空(len=0)没有资源数据
|
||
// start大于dataSize资源读取完成
|
||
if m.start < dataSize {
|
||
var min = func(x, y int) int {
|
||
if x < y {
|
||
return x
|
||
}
|
||
return y
|
||
}
|
||
//把dataOut指针初始化Go类型的切片
|
||
//space切片长度和空间, 使用bytes长度和bytesToRead最小的值
|
||
space := min(dataSize, int(bytesToRead))
|
||
dataOutByteSlice := &reflect.SliceHeader{
|
||
Data: dataOut,
|
||
Len: space,
|
||
Cap: space,
|
||
}
|
||
dst := *(*[]byte)(unsafe.Pointer(dataOutByteSlice))
|
||
//end 计算当前读取资源数据的结束位置
|
||
end := m.start
|
||
//拿出最小的数据长度做为结束位置
|
||
//bytesToRead当前最大读取数量一搬最大值是固定
|
||
if dataSize < int(bytesToRead) {
|
||
end += dataSize
|
||
} else {
|
||
end += int(bytesToRead)
|
||
}
|
||
//如果结束位置大于bytes长度,我们使用bytes长度
|
||
end = min(end, dataSize)
|
||
//把每次分块读取的资源数据复制到dataOut
|
||
c := copy(dst, m.bytes[m.start:end])
|
||
m.start += c //设置下次读取资源开始位置
|
||
bytesRead = int32(c) //读取资源读取字节个数
|
||
return bytesRead, bytesRead > 0
|
||
}
|
||
return
|
||
}
|
||
|
||
// checkRequest = true, 读取bytes, 返回到dataOut
|
||
func (m *source) read(dataOut uintptr, bytesToRead int32, callback *ICefResourceReadCallback) (bytesRead int32, result bool) {
|
||
bytesRead, result = m.out(dataOut, bytesToRead)
|
||
if result {
|
||
//callback.Cont(int64(bytesRead))
|
||
}
|
||
return
|
||
}
|
||
|
||
func (m *source) readResponse(dataOut uintptr, bytesToRead int32, callback *ICefCallback) (bytesRead int32, result bool) {
|
||
bytesRead, result = m.out(dataOut, bytesToRead)
|
||
if !result {
|
||
callback.Cont()
|
||
}
|
||
return
|
||
}
|