mirror of https://github.com/1Panel-dev/1Panel
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
588 lines
16 KiB
588 lines
16 KiB
package service
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
|
"github.com/1Panel-dev/1Panel/backend/buserr"
|
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
|
"github.com/1Panel-dev/1Panel/backend/global"
|
|
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/backend/utils/common"
|
|
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
|
|
"github.com/1Panel-dev/1Panel/backend/utils/xpack"
|
|
"github.com/jinzhu/copier"
|
|
"github.com/robfig/cron/v3"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
clamServiceNameCentOs = "clamd@scan.service"
|
|
clamServiceNameUbuntu = "clamav-daemon.service"
|
|
freshClamService = "clamav-freshclam.service"
|
|
resultDir = "clamav"
|
|
)
|
|
|
|
type ClamService struct {
|
|
serviceName string
|
|
}
|
|
|
|
type IClamService interface {
|
|
LoadBaseInfo() (dto.ClamBaseInfo, error)
|
|
Operate(operate string) error
|
|
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
|
|
Create(req dto.ClamCreate) error
|
|
Update(req dto.ClamUpdate) error
|
|
UpdateStatus(id uint, status string) error
|
|
Delete(req dto.ClamDelete) error
|
|
HandleOnce(req dto.OperateByID) error
|
|
LoadFile(req dto.ClamFileReq) (string, error)
|
|
UpdateFile(req dto.UpdateByNameAndFile) error
|
|
LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error)
|
|
CleanRecord(req dto.OperateByID) error
|
|
|
|
LoadRecordLog(req dto.ClamLogReq) (string, error)
|
|
}
|
|
|
|
func NewIClamService() IClamService {
|
|
return &ClamService{}
|
|
}
|
|
|
|
func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
|
|
var baseInfo dto.ClamBaseInfo
|
|
baseInfo.Version = "-"
|
|
baseInfo.FreshVersion = "-"
|
|
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
|
|
if exist1 {
|
|
c.serviceName = clamServiceNameCentOs
|
|
baseInfo.IsExist = true
|
|
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
|
|
}
|
|
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
|
|
if exist2 {
|
|
c.serviceName = clamServiceNameUbuntu
|
|
baseInfo.IsExist = true
|
|
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
|
|
}
|
|
freshExist, _ := systemctl.IsExist(freshClamService)
|
|
if freshExist {
|
|
baseInfo.FreshIsExist = true
|
|
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService)
|
|
}
|
|
if !cmd.Which("clamdscan") {
|
|
baseInfo.IsActive = false
|
|
}
|
|
|
|
if baseInfo.IsActive {
|
|
version, err := cmd.Exec("clamdscan --version")
|
|
if err == nil {
|
|
if strings.Contains(version, "/") {
|
|
baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
|
|
} else {
|
|
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
|
|
}
|
|
}
|
|
} else {
|
|
_ = StopAllCronJob(false)
|
|
}
|
|
if baseInfo.FreshIsActive {
|
|
version, err := cmd.Exec("freshclam --version")
|
|
if err == nil {
|
|
if strings.Contains(version, "/") {
|
|
baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
|
|
} else {
|
|
baseInfo.FreshVersion = strings.TrimPrefix(version, "ClamAV ")
|
|
}
|
|
}
|
|
}
|
|
return baseInfo, nil
|
|
}
|
|
|
|
func (c *ClamService) Operate(operate string) error {
|
|
switch operate {
|
|
case "start", "restart", "stop":
|
|
stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
|
|
}
|
|
return nil
|
|
case "fresh-start", "fresh-restart", "fresh-stop":
|
|
stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService)
|
|
if err != nil {
|
|
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("not support such operation: %v", operate)
|
|
}
|
|
}
|
|
|
|
func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
|
|
total, commands, err := clamRepo.Page(req.Page, req.PageSize, commonRepo.WithLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
var datas []dto.ClamInfo
|
|
for _, command := range commands {
|
|
var item dto.ClamInfo
|
|
if err := copier.Copy(&item, &command); err != nil {
|
|
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
|
}
|
|
item.LastHandleDate = "-"
|
|
datas = append(datas, item)
|
|
}
|
|
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
|
for i := 0; i < len(datas); i++ {
|
|
logPaths := loadFileByName(datas[i].Name)
|
|
sort.Slice(logPaths, func(i, j int) bool {
|
|
return logPaths[i] > logPaths[j]
|
|
})
|
|
if len(logPaths) != 0 {
|
|
t1, err := time.ParseInLocation(constant.DateTimeSlimLayout, logPaths[0], nyc)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
datas[i].LastHandleDate = t1.Format(constant.DateTimeLayout)
|
|
}
|
|
}
|
|
return total, datas, err
|
|
}
|
|
|
|
func (c *ClamService) Create(req dto.ClamCreate) error {
|
|
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name))
|
|
if clam.ID != 0 {
|
|
return constant.ErrRecordExist
|
|
}
|
|
if err := copier.Copy(&clam, &req); err != nil {
|
|
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
|
}
|
|
if clam.InfectedStrategy == "none" || clam.InfectedStrategy == "remove" {
|
|
clam.InfectedDir = ""
|
|
}
|
|
if len(req.Spec) != 0 {
|
|
entryID, err := xpack.StartClam(clam, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clam.EntryID = entryID
|
|
clam.Status = constant.StatusEnable
|
|
}
|
|
if err := clamRepo.Create(&clam); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ClamService) Update(req dto.ClamUpdate) error {
|
|
clam, _ := clamRepo.Get(commonRepo.WithByName(req.Name))
|
|
if clam.ID == 0 {
|
|
return constant.ErrRecordNotFound
|
|
}
|
|
if req.InfectedStrategy == "none" || req.InfectedStrategy == "remove" {
|
|
req.InfectedDir = ""
|
|
}
|
|
var clamItem model.Clam
|
|
if err := copier.Copy(&clamItem, &req); err != nil {
|
|
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
|
}
|
|
clamItem.EntryID = clam.EntryID
|
|
upMap := map[string]interface{}{}
|
|
if len(clam.Spec) != 0 && clam.EntryID != 0 {
|
|
global.Cron.Remove(cron.EntryID(clamItem.EntryID))
|
|
upMap["entry_id"] = 0
|
|
}
|
|
if len(req.Spec) == 0 {
|
|
upMap["status"] = ""
|
|
upMap["entry_id"] = 0
|
|
}
|
|
if len(req.Spec) != 0 && clam.Status != constant.StatusDisable {
|
|
newEntryID, err := xpack.StartClam(clamItem, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
upMap["entry_id"] = newEntryID
|
|
}
|
|
if len(clam.Spec) == 0 && len(req.Spec) != 0 {
|
|
upMap["status"] = constant.StatusEnable
|
|
}
|
|
|
|
upMap["name"] = req.Name
|
|
upMap["path"] = req.Path
|
|
upMap["infected_dir"] = req.InfectedDir
|
|
upMap["infected_strategy"] = req.InfectedStrategy
|
|
upMap["spec"] = req.Spec
|
|
upMap["description"] = req.Description
|
|
if err := clamRepo.Update(req.ID, upMap); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ClamService) UpdateStatus(id uint, status string) error {
|
|
clam, _ := clamRepo.Get(commonRepo.WithByID(id))
|
|
if clam.ID == 0 {
|
|
return constant.ErrRecordNotFound
|
|
}
|
|
var (
|
|
entryID int
|
|
err error
|
|
)
|
|
if status == constant.StatusEnable {
|
|
entryID, err = xpack.StartClam(clam, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
global.Cron.Remove(cron.EntryID(clam.EntryID))
|
|
global.LOG.Infof("stop cronjob entryID: %v", clam.EntryID)
|
|
}
|
|
|
|
return clamRepo.Update(clam.ID, map[string]interface{}{"status": status, "entry_id": entryID})
|
|
}
|
|
|
|
func (c *ClamService) Delete(req dto.ClamDelete) error {
|
|
for _, id := range req.Ids {
|
|
clam, _ := clamRepo.Get(commonRepo.WithByID(id))
|
|
if clam.ID == 0 {
|
|
continue
|
|
}
|
|
if req.RemoveRecord {
|
|
_ = os.RemoveAll(path.Join(global.CONF.System.DataDir, resultDir, clam.Name))
|
|
}
|
|
if req.RemoveInfected {
|
|
_ = os.RemoveAll(path.Join(clam.InfectedDir, "1panel-infected", clam.Name))
|
|
}
|
|
if err := clamRepo.Delete(commonRepo.WithByID(id)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *ClamService) HandleOnce(req dto.OperateByID) error {
|
|
if cleaned := StopAllCronJob(true); cleaned {
|
|
return buserr.New("ErrClamdscanNotFound")
|
|
}
|
|
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
|
|
if clam.ID == 0 {
|
|
return constant.ErrRecordNotFound
|
|
}
|
|
if cmd.CheckIllegal(clam.Path) {
|
|
return buserr.New(constant.ErrCmdIllegal)
|
|
}
|
|
timeNow := time.Now().Format(constant.DateTimeSlimLayout)
|
|
logFile := path.Join(global.CONF.System.DataDir, resultDir, clam.Name, timeNow)
|
|
if _, err := os.Stat(path.Dir(logFile)); err != nil {
|
|
_ = os.MkdirAll(path.Dir(logFile), os.ModePerm)
|
|
}
|
|
go func() {
|
|
strategy := ""
|
|
switch clam.InfectedStrategy {
|
|
case "remove":
|
|
strategy = "--remove"
|
|
case "move":
|
|
dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow)
|
|
strategy = "--move=" + dir
|
|
if _, err := os.Stat(dir); err != nil {
|
|
_ = os.MkdirAll(dir, os.ModePerm)
|
|
}
|
|
case "copy":
|
|
dir := path.Join(clam.InfectedDir, "1panel-infected", clam.Name, timeNow)
|
|
strategy = "--copy=" + dir
|
|
if _, err := os.Stat(dir); err != nil {
|
|
_ = os.MkdirAll(dir, os.ModePerm)
|
|
}
|
|
}
|
|
global.LOG.Debugf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile)
|
|
stdout, err := cmd.Execf("clamdscan --fdpass %s %s -l %s", strategy, clam.Path, logFile)
|
|
if err != nil {
|
|
global.LOG.Errorf("clamdscan failed, stdout: %v, err: %v", stdout, err)
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (c *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, error) {
|
|
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ClamID))
|
|
if clam.ID == 0 {
|
|
return 0, nil, constant.ErrRecordNotFound
|
|
}
|
|
logPaths := loadFileByName(clam.Name)
|
|
if len(logPaths) == 0 {
|
|
return 0, nil, nil
|
|
}
|
|
|
|
var filterFiles []string
|
|
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
|
for _, item := range logPaths {
|
|
t1, err := time.ParseInLocation(constant.DateTimeSlimLayout, item, nyc)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if t1.After(req.StartTime) && t1.Before(req.EndTime) {
|
|
filterFiles = append(filterFiles, item)
|
|
}
|
|
}
|
|
if len(filterFiles) == 0 {
|
|
return 0, nil, nil
|
|
}
|
|
|
|
sort.Slice(filterFiles, func(i, j int) bool {
|
|
return filterFiles[i] > filterFiles[j]
|
|
})
|
|
|
|
var records []string
|
|
total, start, end := len(filterFiles), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
|
if start > total {
|
|
records = make([]string, 0)
|
|
} else {
|
|
if end >= total {
|
|
end = total
|
|
}
|
|
records = filterFiles[start:end]
|
|
}
|
|
|
|
var datas []dto.ClamLog
|
|
for i := 0; i < len(records); i++ {
|
|
item := loadResultFromLog(path.Join(global.CONF.System.DataDir, resultDir, clam.Name, records[i]))
|
|
datas = append(datas, item)
|
|
}
|
|
return int64(total), datas, nil
|
|
}
|
|
func (c *ClamService) LoadRecordLog(req dto.ClamLogReq) (string, error) {
|
|
logPath := path.Join(global.CONF.System.DataDir, resultDir, req.ClamName, req.RecordName)
|
|
var tail string
|
|
if req.Tail != "0" {
|
|
tail = req.Tail
|
|
} else {
|
|
tail = "+1"
|
|
}
|
|
cmd := exec.Command("tail", "-n", tail, logPath)
|
|
stdout, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
|
|
}
|
|
return string(stdout), nil
|
|
}
|
|
|
|
func (c *ClamService) CleanRecord(req dto.OperateByID) error {
|
|
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
|
|
if clam.ID == 0 {
|
|
return constant.ErrRecordNotFound
|
|
}
|
|
pathItem := path.Join(global.CONF.System.DataDir, resultDir, clam.Name)
|
|
_ = os.RemoveAll(pathItem)
|
|
return nil
|
|
}
|
|
|
|
func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
|
|
filePath := ""
|
|
switch req.Name {
|
|
case "clamd":
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
filePath = "/etc/clamav/clamd.conf"
|
|
} else {
|
|
filePath = "/etc/clamd.d/scan.conf"
|
|
}
|
|
case "clamd-log":
|
|
filePath = c.loadLogPath("clamd-log")
|
|
if len(filePath) != 0 {
|
|
break
|
|
}
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
filePath = "/var/log/clamav/clamav.log"
|
|
} else {
|
|
filePath = "/var/log/clamd.scan"
|
|
}
|
|
case "freshclam":
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
filePath = "/etc/clamav/freshclam.conf"
|
|
} else {
|
|
filePath = "/etc/freshclam.conf"
|
|
}
|
|
case "freshclam-log":
|
|
filePath = c.loadLogPath("freshclam-log")
|
|
if len(filePath) != 0 {
|
|
break
|
|
}
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
filePath = "/var/log/clamav/freshclam.log"
|
|
} else {
|
|
filePath = "/var/log/freshclam.log"
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("not support such type")
|
|
}
|
|
if _, err := os.Stat(filePath); err != nil {
|
|
return "", buserr.New("ErrHttpReqNotFound")
|
|
}
|
|
var tail string
|
|
if req.Tail != "0" {
|
|
tail = req.Tail
|
|
} else {
|
|
tail = "+1"
|
|
}
|
|
cmd := exec.Command("tail", "-n", tail, filePath)
|
|
stdout, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
|
|
}
|
|
return string(stdout), nil
|
|
}
|
|
|
|
func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
|
|
filePath := ""
|
|
service := ""
|
|
switch req.Name {
|
|
case "clamd":
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
service = clamServiceNameUbuntu
|
|
filePath = "/etc/clamav/clamd.conf"
|
|
} else {
|
|
service = clamServiceNameCentOs
|
|
filePath = "/etc/clamd.d/scan.conf"
|
|
}
|
|
case "freshclam":
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
filePath = "/etc/clamav/freshclam.conf"
|
|
} else {
|
|
filePath = "/etc/freshclam.conf"
|
|
}
|
|
service = "clamav-freshclam.service"
|
|
default:
|
|
return fmt.Errorf("not support such type")
|
|
}
|
|
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
write := bufio.NewWriter(file)
|
|
_, _ = write.WriteString(req.File)
|
|
write.Flush()
|
|
|
|
_ = systemctl.Restart(service)
|
|
return nil
|
|
}
|
|
|
|
func StopAllCronJob(withCheck bool) bool {
|
|
if withCheck {
|
|
isActive := false
|
|
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
|
|
if exist1 {
|
|
isActive, _ = systemctl.IsActive(clamServiceNameCentOs)
|
|
}
|
|
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
|
|
if exist2 {
|
|
isActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
|
|
}
|
|
if isActive {
|
|
return false
|
|
}
|
|
}
|
|
clams, _ := clamRepo.List(commonRepo.WithByStatus(constant.StatusEnable))
|
|
for i := 0; i < len(clams); i++ {
|
|
global.Cron.Remove(cron.EntryID(clams[i].EntryID))
|
|
_ = clamRepo.Update(clams[i].ID, map[string]interface{}{"status": constant.StatusDisable, "entry_id": 0})
|
|
}
|
|
return true
|
|
}
|
|
|
|
func loadFileByName(name string) []string {
|
|
var logPaths []string
|
|
pathItem := path.Join(global.CONF.System.DataDir, resultDir, name)
|
|
_ = filepath.Walk(pathItem, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if info.IsDir() || info.Name() == name {
|
|
return nil
|
|
}
|
|
logPaths = append(logPaths, info.Name())
|
|
return nil
|
|
})
|
|
return logPaths
|
|
}
|
|
func loadResultFromLog(pathItem string) dto.ClamLog {
|
|
var data dto.ClamLog
|
|
data.Name = path.Base(pathItem)
|
|
data.Status = constant.StatusWaiting
|
|
file, err := os.ReadFile(pathItem)
|
|
if err != nil {
|
|
return data
|
|
}
|
|
lines := strings.Split(string(file), "\n")
|
|
for _, line := range lines {
|
|
if strings.Contains(line, "- SCAN SUMMARY -") {
|
|
data.Status = constant.StatusDone
|
|
}
|
|
if data.Status != constant.StatusDone {
|
|
continue
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(line, "Infected files:"):
|
|
data.InfectedFiles = strings.TrimPrefix(line, "Infected files:")
|
|
case strings.HasPrefix(line, "Total errors:"):
|
|
data.TotalError = strings.TrimPrefix(line, "Total errors:")
|
|
case strings.HasPrefix(line, "Time:"):
|
|
if strings.Contains(line, "(") {
|
|
data.ScanTime = strings.ReplaceAll(strings.Split(line, "(")[1], ")", "")
|
|
continue
|
|
}
|
|
data.ScanTime = strings.TrimPrefix(line, "Time:")
|
|
case strings.HasPrefix(line, "Start Date:"):
|
|
data.ScanDate = strings.TrimPrefix(line, "Start Date:")
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
func (c *ClamService) loadLogPath(name string) string {
|
|
confPath := ""
|
|
if name == "clamd-log" {
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
confPath = "/etc/clamav/clamd.conf"
|
|
} else {
|
|
confPath = "/etc/clamd.d/scan.conf"
|
|
}
|
|
} else {
|
|
if c.serviceName == clamServiceNameUbuntu {
|
|
confPath = "/etc/clamav/freshclam.conf"
|
|
} else {
|
|
confPath = "/etc/freshclam.conf"
|
|
}
|
|
}
|
|
if _, err := os.Stat(confPath); err != nil {
|
|
return ""
|
|
}
|
|
content, err := os.ReadFile(confPath)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
lines := strings.Split(string(content), "\n")
|
|
if name == "clamd-log" {
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "LogFile ") {
|
|
return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ")
|
|
}
|
|
}
|
|
} else {
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "UpdateLogFile ") {
|
|
return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ")
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|