feat: description file for custom theme; use gjson (#433)

* feat: description file for custom theme; use gjson

* fix gosec

* remove outdated stuff
This commit is contained in:
UUBulb 2024-10-10 00:08:16 +08:00 committed by GitHub
parent 937696c26d
commit 55f5c89c1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 216 additions and 161 deletions

View File

@ -3,10 +3,10 @@ package controller
import (
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@ -34,28 +34,16 @@ func ServeWeb(port uint) *http.Server {
pprof.Register(r)
}
r.Use(natGateway)
if os.Getenv("NZ_LOCAL_TEMPLATE") == "true" {
r.SetFuncMap(funcMap)
r.Use(mygin.RecordPath)
r.Static("/static", "resource/static")
r.LoadHTMLGlob("resource/template/**/*.html")
} else {
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
staticFs, err := fs.Sub(resource.StaticFS, "static")
if err != nil {
panic(err)
}
r.StaticFS("/static", http.FS(staticFs))
tmpl := template.New("").Funcs(funcMap)
var err error
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
if err != nil {
panic(err)
}
r.Static("/static-custom", "resource/static/custom")
tmpl = loadThirdPartyTemplates(tmpl)
r.SetHTMLTemplate(tmpl)
r.Use(mygin.RecordPath)
r.StaticFS("/static", http.FS(resource.StaticFS))
routers(r)
page404 := func(c *gin.Context) {
mygin.ShowErrorPage(c, mygin.ErrInfo{
@ -106,14 +94,40 @@ func loadThirdPartyTemplates(tmpl *template.Template) *template.Template {
if !theme.IsDir() {
continue
}
// load templates
t, err := ret.ParseGlob(fmt.Sprintf("resource/template/%s/*.html", theme.Name()))
if err != nil {
log.Printf("NEZHA>> Error parsing templates %s error: %v", theme.Name(), err)
themeDir := theme.Name()
if !strings.HasPrefix(themeDir, "theme-") {
log.Printf("NEZHA>> Invalid theme name: %s", themeDir)
continue
}
descPath := filepath.Join("resource", "template", themeDir, "theme.json")
desc, err := os.ReadFile(filepath.Clean(descPath))
if err != nil {
log.Printf("NEZHA>> Error opening %s config: %v", themeDir, err)
continue
}
themeName, err := utils.GjsonGet(desc, "name")
if err != nil {
log.Printf("NEZHA>> Error opening %s config: not a valid description file", theme.Name())
continue
}
// load templates
templatePath := filepath.Join("resource", "template", themeDir, "*.html")
t, err := ret.ParseGlob(templatePath)
if err != nil {
log.Printf("NEZHA>> Error parsing templates %s: %v", themeDir, err)
continue
}
themeKey := strings.TrimPrefix(themeDir, "theme-")
model.Themes[themeKey] = themeName.String()
ret = t
}
return ret
}

3
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/tidwall/gjson v1.18.0
github.com/xanzy/go-gitlab v0.103.0
golang.org/x/crypto v0.25.0
golang.org/x/net v0.27.0
@ -70,6 +71,8 @@ require (
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect

6
go.sum
View File

@ -180,6 +180,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=

View File

@ -23,7 +23,6 @@ var Themes = map[string]string{
"hotaru": "Hotaru",
"angel-kanade": "AngelKanade",
"server-status": "ServerStatus",
"custom": "Custom(local)",
}
var DashboardThemes = map[string]string{

View File

@ -71,8 +71,8 @@ func (ns *NotificationServerBundle) reqBody(message string) (string, error) {
return string(msgBytes)[1 : len(msgBytes)-1]
}), nil
case NotificationRequestTypeForm:
var data map[string]string
if err := utils.Json.Unmarshal([]byte(n.RequestBody), &data); err != nil {
data, err := utils.GjsonParseStringMap(n.RequestBody)
if err != nil {
return "", err
}
params := url.Values{}
@ -99,8 +99,8 @@ func (n *Notification) setRequestHeader(req *http.Request) error {
if n.RequestHeader == "" {
return nil
}
var m map[string]string
if err := utils.Json.Unmarshal([]byte(n.RequestHeader), &m); err != nil {
m, err := utils.GjsonParseStringMap(n.RequestHeader)
if err != nil {
return err
}
for k, v := range m {

View File

@ -2,6 +2,7 @@ package ddns
import (
"bytes"
"errors"
"fmt"
"io"
"log"
@ -14,10 +15,13 @@ import (
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
type ProviderCloudflare struct {
isIpv4 bool
domainConfig *DomainConfig
secret string
zoneId string
ipAddr string
recordId string
domainConfig *DomainConfig
recordType string
}
type cfReq struct {
@ -28,13 +32,6 @@ type cfReq struct {
Proxied bool `json:"proxied"`
}
type cfResp struct {
Result []struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"result"`
}
func NewProviderCloudflare(s string) *ProviderCloudflare {
return &ProviderCloudflare{
secret: s,
@ -54,13 +51,19 @@ func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) err
// 当IPv4和IPv6同时成功才算作成功
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
provider.isIpv4 = true
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
provider.isIpv4 = false
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
@ -68,19 +71,18 @@ func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) err
return nil
}
func (provider *ProviderCloudflare) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
func (provider *ProviderCloudflare) addDomainRecord() error {
err := provider.findDNSRecord()
if err != nil {
if errors.Is(err, utils.ErrGjsonNotFound) {
// 添加 DNS 记录
return provider.createDNSRecord()
}
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if provider.recordId == "" {
// 添加 DNS 记录
return provider.createDNSRecord(isIpv4)
} else {
// 更新 DNS 记录
return provider.updateDNSRecord(isIpv4)
}
// 更新 DNS 记录
return provider.updateDNSRecord()
}
func (provider *ProviderCloudflare) getZoneID() error {
@ -96,35 +98,22 @@ func (provider *ProviderCloudflare) getZoneID() error {
return err
}
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
result, err := utils.GjsonGet(body, "result.0.id")
if err != nil {
return err
}
result := res.Result
if len(result) > 0 {
provider.zoneId = result[0].ID
return nil
}
return fmt.Errorf("找不到 Zone ID")
provider.zoneId = result.String()
return nil
}
func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
func (provider *ProviderCloudflare) findDNSRecord() error {
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
du, _ := url.Parse(de)
q := du.Query()
q.Set("name", provider.domainConfig.FullDomain)
q.Set("type", ipType)
q.Set("type", provider.recordType)
du.RawQuery = q.Encode()
body, err := provider.sendRequest("GET", du.String(), nil)
@ -132,36 +121,21 @@ func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
return err
}
res := &cfResp{}
err = utils.Json.Unmarshal(body, res)
result, err := utils.GjsonGet(body, "result.0.id")
if err != nil {
return err
}
result := res.Result
if len(result) > 0 {
provider.recordId = result[0].ID
return nil
}
provider.recordId = result.String()
return nil
}
func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
func (provider *ProviderCloudflare) createDNSRecord() error {
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
Type: provider.recordType,
Content: provider.ipAddr,
TTL: 60,
Proxied: false,
}
@ -171,21 +145,12 @@ func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
return err
}
func (provider *ProviderCloudflare) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
func (provider *ProviderCloudflare) updateDNSRecord() error {
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
data := &cfReq{
Name: provider.domainConfig.FullDomain,
Type: ipType,
Content: ipAddr,
Type: provider.recordType,
Content: provider.ipAddr,
TTL: 60,
Proxied: false,
}

View File

@ -20,3 +20,10 @@ func splitDomain(domain string) (prefix string, realDomain string) {
prefix = domain[:len(domain)-len(realDomain)-1]
return prefix, realDomain
}
func getRecordString(isIpv4 bool) string {
if isIpv4 {
return "A"
}
return "AAAA"
}

View File

@ -5,6 +5,7 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
@ -19,10 +20,14 @@ import (
const te = "https://dnspod.tencentcloudapi.com"
type ProviderTencentCloud struct {
isIpv4 bool
domainConfig *DomainConfig
recordID uint64
recordType string
secretID string
secretKey string
domainConfig *DomainConfig
resp *tcResp
errCode string
ipAddr string
}
type tcReq struct {
@ -36,18 +41,6 @@ type tcReq struct {
RecordId uint64 `json:"RecordId,omitempty"`
}
type tcResp struct {
Response struct {
RecordList []struct {
RecordId uint64
Value string
}
Error struct {
Code string
}
}
}
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
return &ProviderTencentCloud{
secretID: id,
@ -64,13 +57,19 @@ func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) e
// 当IPv4和IPv6同时成功才算作成功
var err error
if provider.domainConfig.EnableIPv4 {
if err = provider.addDomainRecord(true); err != nil {
provider.isIpv4 = true
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv4Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
if provider.domainConfig.EnableIpv6 {
if err = provider.addDomainRecord(false); err != nil {
provider.isIpv4 = false
provider.recordType = getRecordString(provider.isIpv4)
provider.ipAddr = provider.domainConfig.Ipv6Addr
if err = provider.addDomainRecord(); err != nil {
return err
}
}
@ -78,33 +77,26 @@ func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) e
return err
}
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error {
err := provider.findDNSRecord(isIpv4)
func (provider *ProviderTencentCloud) addDomainRecord() error {
err := provider.findDNSRecord()
if err != nil {
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
}
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
return provider.createDNSRecord(isIpv4)
} else if provider.resp.Response.Error.Code != "" {
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code)
if provider.errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
return provider.createDNSRecord()
} else if provider.errCode != "" {
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.errCode)
}
// 默认情况下更新 DNS 记录
return provider.updateDNSRecord(isIpv4)
return provider.updateDNSRecord()
}
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
var ipType string
if isIPv4 {
ipType = "A"
} else {
ipType = "AAAA"
}
func (provider *ProviderTencentCloud) findDNSRecord() error {
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordType: provider.recordType,
Domain: realDomain,
RecordLine: "默认",
Subdomain: prefix,
@ -116,32 +108,29 @@ func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
return err
}
provider.resp = &tcResp{}
err = utils.Json.Unmarshal(body, provider.resp)
result, err := utils.GjsonGet(body, "Response.RecordList.0.RecordId")
if err != nil {
if errors.Is(err, utils.ErrGjsonNotFound) {
if errCode, err := utils.GjsonGet(body, "Response.Error.Code"); err == nil {
provider.errCode = errCode.String()
return nil
}
}
return err
}
provider.recordID = result.Uint()
return nil
}
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
func (provider *ProviderTencentCloud) createDNSRecord() error {
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordType: provider.recordType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
Value: provider.ipAddr,
TTL: 600,
}
@ -150,25 +139,16 @@ func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
return err
}
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error {
var ipType, ipAddr string
if isIPv4 {
ipType = "A"
ipAddr = provider.domainConfig.Ipv4Addr
} else {
ipType = "AAAA"
ipAddr = provider.domainConfig.Ipv6Addr
}
func (provider *ProviderTencentCloud) updateDNSRecord() error {
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
data := &tcReq{
RecordType: ipType,
RecordType: provider.recordType,
RecordLine: "默认",
Domain: realDomain,
SubDomain: prefix,
Value: ipAddr,
Value: provider.ipAddr,
TTL: 600,
RecordId: provider.resp.Response.RecordList[0].RecordId,
RecordId: provider.recordID,
}
jsonData, _ := utils.Json.Marshal(data)

36
pkg/utils/gjson.go Normal file
View File

@ -0,0 +1,36 @@
package utils
import (
"errors"
"github.com/tidwall/gjson"
)
var (
ErrGjsonNotFound = errors.New("specified path does not exist")
ErrGjsonWrongType = errors.New("wrong type")
)
func GjsonGet(json []byte, path string) (gjson.Result, error) {
result := gjson.GetBytes(json, path)
if !result.Exists() {
return result, ErrGjsonNotFound
}
return result, nil
}
func GjsonParseStringMap(jsonObject string) (map[string]string, error) {
result := gjson.Parse(jsonObject)
if !result.IsObject() {
return nil, ErrGjsonWrongType
}
ret := make(map[string]string)
result.ForEach(func(key, value gjson.Result) bool {
ret[key.String()] = value.String()
return true
})
return ret, nil
}

33
pkg/utils/hfs.go Normal file
View File

@ -0,0 +1,33 @@
package utils
import (
"embed"
"io/fs"
"os"
)
// HybridFS combines embed.FS and os.DirFS.
type HybridFS struct {
embedFS, dir fs.FS
}
func NewHybridFS(embed embed.FS, subDir string, localDir string) (*HybridFS, error) {
subFS, err := fs.Sub(embed, subDir)
if err != nil {
return nil, err
}
return &HybridFS{
embedFS: subFS,
dir: os.DirFS(localDir),
}, nil
}
func (hfs *HybridFS) Open(name string) (fs.File, error) {
// Ensure embed files are not replaced
if file, err := hfs.embedFS.Open(name); err == nil {
return file, nil
}
return hfs.dir.Open(name)
}

14
resource/resource.go vendored
View File

@ -2,10 +2,14 @@ package resource
import (
"embed"
"github.com/naiba/nezha/pkg/utils"
)
var StaticFS *utils.HybridFS
//go:embed static
var StaticFS embed.FS
var staticFS embed.FS
//go:embed template
var TemplateFS embed.FS
@ -13,6 +17,14 @@ var TemplateFS embed.FS
//go:embed l10n
var I18nFS embed.FS
func init() {
var err error
StaticFS, err = utils.NewHybridFS(staticFS, "static", "resource/static/custom")
if err != nil {
panic(err)
}
}
func IsTemplateFileExist(name string) bool {
_, err := TemplateFS.Open(name)
return err == nil

View File

@ -155,8 +155,8 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
Ipv4Addr: ipv4,
Ipv6Addr: ipv6,
}
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
go singleton.RetryableUpdateDomain(provider, config, maxRetries)
} else {
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)