From b606431305233a8bd1a25b0489689bac7d696be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Tue, 11 Jul 2023 01:26:04 +0800 Subject: [PATCH] feat: system helpers and website service --- README.md | 2 +- app/services/website.go | 210 +++++++++++++++++++++++++++++++- packages/helpers/string.go | 16 ++- packages/helpers/system.go | 109 +++++++++++++++++ packages/helpers/system_test.go | 92 ++++++++++++++ 5 files changed, 422 insertions(+), 7 deletions(-) create mode 100644 packages/helpers/system.go create mode 100644 packages/helpers/system_test.go diff --git a/README.md b/README.md index 5b83a388..40372d06 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ TODO ## 问题反馈 -使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forum) 提问寻求帮助。 +使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forums) 提问寻求帮助。 对于面板自身问题,可在 GitHub 的`Issues` 页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。 diff --git a/app/services/website.go b/app/services/website.go index 9d4c4264..4d15417c 100644 --- a/app/services/website.go +++ b/app/services/website.go @@ -1,7 +1,17 @@ // Package services 网站服务 package services -import "panel/app/models" +import ( + "errors" + "fmt" + "strings" + + "github.com/goravel/framework/facades" + "golang.org/x/exp/slices" + + "panel/app/models" + "panel/packages/helpers" +) type Website interface { List() ([]models.Website, error) @@ -9,10 +19,11 @@ type Website interface { type PanelWebsite struct { Name string `json:"name"` + Status bool `json:"status"` Domain string `json:"domain"` Path string `json:"path"` - Php string `json:"php"` - Ssl string `json:"ssl"` + Php int `json:"php"` + Ssl bool `json:"ssl"` Remark string `json:"remark"` Db bool `json:"db"` DbType string `json:"db_type"` @@ -43,3 +54,196 @@ type WebsiteSetting struct { Raw string `json:"raw"` Log string `json:"log"` } + +type WebsiteImpl struct { +} + +func NewWebsiteImpl() *WebsiteImpl { + return &WebsiteImpl{} +} + +// List 列出网站 +func (r *WebsiteImpl) List(page, limit int) (int64, []models.Website, error) { + var websites []models.Website + var total int64 + if err := facades.Orm().Query().Paginate(page, limit, &websites, &total); err != nil { + return total, websites, err + } + + return total, websites, nil +} + +// Add 添加网站 +func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) { + // 禁止部分保留名称 + nameSlices := []string{"phpmyadmin", "mysql", "panel", "ssh"} + if slices.Contains(nameSlices, website.Name) { + return models.Website{}, errors.New("网站名称" + website.Name + "为保留名称,请更换") + } + + // path为空时,设置默认值 + if len(website.Path) == 0 { + website.Path = "/www/wwwroot/" + website.Name + } + // path不为/开头时,返回错误 + if website.Path[0] != '/' { + return models.Website{}, errors.New("网站路径" + website.Path + "必须以/开头") + } + + website.Ssl = false + website.Status = true + website.Domain = strings.TrimSpace(website.Domain) + + w := models.Website{ + Name: website.Name, + Status: website.Status, + Path: website.Path, + Php: website.Php, + Ssl: website.Ssl, + Remark: website.Remark, + } + if err := facades.Orm().Query().Create(&w); err != nil { + return w, err + } + + helpers.Mkdir(website.Path, 0755) + + index := ` + + + + +耗子Linux面板 + + +

耗子Linux面板

+

这是耗子Linux面板的网站默认页面!

+

当您看到此页面,说明您的网站已创建成功。

+ + + +` + helpers.WriteFile(website.Path+"/index.html", index, 0644) + + domainArr := strings.Split(website.Domain, "\n") + portList := "" + portArr := make(map[string]bool) + domainList := "" + for key, value := range domainArr { + temp := strings.Split(value, ":") + domainList += " " + temp[0] + + if len(temp) < 2 { + if _, ok := portArr["80"]; !ok { + if key == len(domainArr)-1 { + portList += " listen 80;" + } else { + portList += " listen 80;\n" + } + portArr["80"] = true + } + } else { + if _, ok := portArr[temp[1]]; !ok { + if key == len(domainArr)-1 { + portList += " listen " + temp[1] + ";" + } else { + portList += " listen " + temp[1] + ";\n" + } + portArr[temp[1]] = true + } + } + } + + nginxConf := fmt.Sprintf(` +# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别! +# 有自定义配置需求的,请将自定义的配置写在各标记位下方。 +server +{ + # port标记位开始 +%s + # port标记位结束 + # server_name标记位开始 + server_name%s; + # server_name标记位结束 + # index标记位开始 + index index.php index.html; + # index标记位结束 + # root标记位开始 + root %s; + # root标记位结束 + + # ssl标记位开始 + # ssl标记位结束 + + # php标记位开始 + include enable-php-%d.conf; + # php标记位结束 + + # waf标记位开始 + waf on; + waf_rule_path /www/server/nginx/ngx_waf/assets/rules/; + waf_mode DYNAMIC; + waf_cc_deny rate=1000r/m duration=60m; + waf_cache capacity=50; + # waf标记位结束 + + # 错误页配置,可自行设置 + #error_page 404 /404.html; + #error_page 502 /502.html; + + # 伪静态规则引入,修改后将导致面板设置的伪静态规则失效 + include /www/server/vhost/openresty/rewrite/%s.conf; + + # 面板默认禁止访问部分敏感目录,可自行修改 + location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn) + { + return 404; + } + # 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改 + location ~ .*\.(js|css)$ + { + expires 1h; + error_log /dev/null; + access_log /dev/null; + } + access_log /www/wwwlogs/%s.log; + error_log /www/wwwlogs/%s.log; +} + +`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name) + + helpers.WriteFile("/www/server/panel/vhost/openresty/"+website.Name+".conf", nginxConf, 0644) + helpers.WriteFile("/www/server/panel/vhost/openresty/rewrite/"+website.Name+".conf", "", 0644) + helpers.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".pem", "", 0644) + helpers.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".key", "", 0644) + + helpers.ExecShellAsync("systemctl reload openresty") + + // TODO 创建数据库 + + return w, nil +} + +// Delete 删除网站 +func (r *WebsiteImpl) Delete(name string) error { + var website models.Website + if err := facades.Orm().Query().Where("name", name).First(&website); err != nil { + return err + } + + if _, err := facades.Orm().Query().Delete(&website); err != nil { + return err + } + + helpers.RemoveFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf") + helpers.RemoveFile("/www/server/panel/vhost/openresty/rewrite/" + website.Name + ".conf") + helpers.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".pem") + helpers.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".key") + helpers.RemoveFile(website.Path) + + helpers.ExecShellAsync("systemctl reload openresty") + + // TODO 删除数据库 + + return nil +} diff --git a/packages/helpers/string.go b/packages/helpers/string.go index c80bbc12..0743d75b 100644 --- a/packages/helpers/string.go +++ b/packages/helpers/string.go @@ -90,7 +90,17 @@ func FormatBytes(size float64) string { // Cut 裁剪字符串 func Cut(begin, end, str string) string { - b := utf8.RuneCountInString(str[:strings.Index(str, begin)]) + utf8.RuneCountInString(begin) - e := utf8.RuneCountInString(str[:strings.Index(str, end)]) - b - return string([]rune(str)[b : b+e]) + bIndex := strings.Index(str, begin) + eIndex := strings.Index(str, end) + if bIndex == -1 || eIndex == -1 || bIndex > eIndex { + return "" + } + + b := utf8.RuneCountInString(str[:bIndex]) + utf8.RuneCountInString(begin) + e := utf8.RuneCountInString(str[:eIndex]) + if b > e { + return "" + } + + return string([]rune(str)[b:e]) } diff --git a/packages/helpers/system.go b/packages/helpers/system.go new file mode 100644 index 00000000..1e14b61a --- /dev/null +++ b/packages/helpers/system.go @@ -0,0 +1,109 @@ +package helpers + +import ( + "os" + "os/exec" + "path/filepath" + + "github.com/goravel/framework/facades" +) + +// WriteFile 写入文件 +func WriteFile(path string, data string, permission os.FileMode) bool { + if err := os.MkdirAll(filepath.Dir(path), permission); err != nil { + facades.Log().Errorf("[面板][Helpers] 创建目录失败: %s", err.Error()) + return false + } + + err := os.WriteFile(path, []byte(data), permission) + if err != nil { + facades.Log().Errorf("[面板][Helpers] 写入文件 %s 失败: %s", path, err.Error()) + return false + } + + return true +} + +// ReadFile 读取文件 +func ReadFile(path string) string { + data, err := os.ReadFile(path) + if err != nil { + facades.Log().Errorf("[面板][Helpers] 读取文件 %s 失败: %s", path, err.Error()) + return "" + } + + return string(data) +} + +// RemoveFile 删除文件 +func RemoveFile(path string) bool { + if err := os.Remove(path); err != nil { + facades.Log().Errorf("[面板][Helpers] 删除文件 %s 失败: %s", path, err.Error()) + return false + } + + return true +} + +// ExecShell 执行 shell 命令 +func ExecShell(shell string) string { + cmd := exec.Command("bash", "-c", shell) + + output, err := cmd.CombinedOutput() + if err != nil { + facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error()) + return "" + } + + return string(output) +} + +// ExecShellAsync 异步执行 shell 命令 +func ExecShellAsync(shell string) { + cmd := exec.Command("bash", "-c", shell) + + err := cmd.Start() + if err != nil { + facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error()) + } + + go func() { + err := cmd.Wait() + if err != nil { + facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error()) + } + }() +} + +// Mkdir 创建目录 +func Mkdir(path string, permission os.FileMode) bool { + if err := os.MkdirAll(path, permission); err != nil { + facades.Log().Errorf("[面板][Helpers] 创建目录 %s 失败: %s", path, err.Error()) + return false + } + + return true +} + +// Chmod 修改文件权限 +func Chmod(path string, permission os.FileMode) bool { + if err := os.Chmod(path, permission); err != nil { + facades.Log().Errorf("[面板][Helpers] 修改文件 %s 权限失败: %s", path, err.Error()) + return false + } + + return true +} + +// Chown 修改路径所有者 +func Chown(path, user, group string) bool { + cmd := exec.Command("chown", "-R", user+":"+group, path) + + err := cmd.Run() + if err != nil { + facades.Log().Errorf("[面板][Helpers] 修改路径 %s 所有者失败: %s", path, err.Error()) + return false + } + + return true +} diff --git a/packages/helpers/system_test.go b/packages/helpers/system_test.go new file mode 100644 index 00000000..db4d4592 --- /dev/null +++ b/packages/helpers/system_test.go @@ -0,0 +1,92 @@ +package helpers + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type SystemHelperTestSuite struct { + suite.Suite +} + +func TestSystemHelperTestSuite(t *testing.T) { + suite.Run(t, &SystemHelperTestSuite{}) +} + +func (s *SystemHelperTestSuite) TestWriteFile() { + filePath := "/tmp/testfile" + defer os.Remove(filePath) + + s.True(WriteFile(filePath, "test data", 0644)) + s.FileExists(filePath) + + content, _ := os.ReadFile(filePath) + s.Equal("test data", string(content)) +} + +func (s *SystemHelperTestSuite) TestReadFile() { + filePath := "/tmp/testfile" + defer os.Remove(filePath) + + err := os.WriteFile(filePath, []byte("test data"), 0644) + s.Nil(err) + + s.Equal("test data", ReadFile(filePath)) +} + +func (s *SystemHelperTestSuite) TestRemoveFile() { + filePath := "/tmp/testfile" + defer os.Remove(filePath) + + err := os.WriteFile(filePath, []byte("test data"), 0644) + s.Nil(err) + + s.True(RemoveFile(filePath)) + s.False(RemoveFile(filePath)) +} + +func (s *SystemHelperTestSuite) TestExecShell() { + s.Equal("test\n", ExecShell("echo 'test'")) +} + +func (s *SystemHelperTestSuite) TestExecShellAsync() { + command := "echo 'test' > /tmp/testfile" + defer os.Remove("/tmp/testfile") + + ExecShellAsync(command) + + time.Sleep(time.Second) + + content, _ := os.ReadFile("/tmp/testfile") + s.Equal("test\n", string(content)) +} + +func (s *SystemHelperTestSuite) TestMkdir() { + dirPath := "/tmp/testdir" + defer os.RemoveAll(dirPath) + + s.True(Mkdir(dirPath, 0755)) +} + +func (s *SystemHelperTestSuite) TestChmod() { + filePath := "/tmp/testfile" + defer os.Remove(filePath) + + err := os.WriteFile(filePath, []byte("test data"), 0644) + s.Nil(err) + + s.True(Chmod(filePath, 0755)) +} + +func (s *SystemHelperTestSuite) TestChown() { + filePath := "/tmp/testfile" + defer os.Remove(filePath) + + err := os.WriteFile(filePath, []byte("test data"), 0644) + s.Nil(err) + + s.True(Chown(filePath, "runner", "runner")) +}